Saltar al contenido

Seguridad de la cadena de suministro para dependencias de software

Protege tu cadena de suministro con versiones fijadas, verificación de lockfiles, generación de SBOM, escaneo automático y control del riesgo externo.

5 min de lectura
Diagrama de seguridad de la cadena de suministro de software que muestra la resolución de dependencias, la verificación de integridad, el escaneo de vulnerabilidades y los puntos de control de generación de SBOM en un pipeline de CI/CD

Tu aplicación es 5% tu código y 95% dependencias. Un solo paquete comprometido —como event-stream, ua-parser-js o colors.js— puede inyectar malware en cada aplicación que dependa de él. Los ataques a la cadena de suministro apuntan al eslabón más débil: la relación de confianza entre los desarrolladores y los paquetes de código abierto que instalan sin revisar.

Defenderse de esto requiere controles en capas: fijar versiones exactas, verificar la integridad, escanear en busca de vulnerabilidades conocidas, generar Listas de Materiales de Software (SBOM) y monitorear continuamente los problemas recién divulgados en tu árbol de dependencias.

Fijación de dependencias e integridad del lockfile

Los lockfiles garantizan instalaciones reproducibles. Pero un lockfile solo es tan confiable como el proceso que lo genera.

jsonjson
// ❌ package.json with loose version ranges
{
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "~4.17.0",
    "axios": "*"
  }
}
// Any install could pull different versions
// A compromised patch release gets pulled automatically
jsonjson
// ✅ Exact versions + lockfile verification
{
  "dependencies": {
    "express": "4.18.2",
    "lodash": "4.17.21",
    "axios": "1.6.2"
  }
}
ymlyaml
# CI pipeline: verify lockfile integrity
# .github/workflows/security.yml
name: Supply Chain Security
 
on: [push, pull_request]
 
jobs:
  verify-dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      # Fail if lockfile doesn't match package.json
      - name: Verify lockfile integrity
        run: npm ci --ignore-scripts
        # npm ci fails if lockfile is out of sync
        # --ignore-scripts prevents postinstall attacks
 
      # Verify package integrity checksums
      - name: Audit dependencies
        run: npm audit --audit-level=high
 
      # Check for known malicious packages
      - name: Socket security check
        uses: SocketDev/socket-security-action@v1
        with:
          api_key: ${{ secrets.SOCKET_API_KEY }}

La bandera --ignore-scripts en CI es fundamental. Los scripts de post-instalación son el principal vector de ataque para paquetes comprometidos: se ejecutan como código arbitrario durante npm install. Desactívalos en CI y permítelos solo para paquetes explícitamente confiables.

Generación de SBOM

Una Lista de Materiales de Software (SBOM) cataloga cada componente de tu aplicación. Muchos marcos de cumplimiento la exigen, y es esencial para responder rápidamente cuando se divulga una nueva vulnerabilidad.

tstypescript
// SBOM generation as part of the build pipeline
interface SBOMComponent {
  name: string;
  version: string;
  type: "library" | "framework" | "application";
  purl: string; // Package URL - universal identifier
  licenses: string[];
  supplier: string;
  hashes: {
    algorithm: string;
    value: string;
  }[];
  dependencies: string[]; // PURLs of direct dependencies
}
 
interface SBOM {
  format: "CycloneDX" | "SPDX";
  version: string;
  metadata: {
    timestamp: string;
    component: {
      name: string;
      version: string;
    };
    tools: string[];
  };
  components: SBOMComponent[];
}
ymlyaml
# Generate SBOM in CI/CD
- name: Generate CycloneDX SBOM
  run: npx @cyclonedx/cyclonedx-npm --output-file sbom.json
 
# Store SBOM as build artifact
- name: Upload SBOM
  uses: actions/upload-artifact@v4
  with:
    name: sbom
    path: sbom.json
 
# Scan SBOM for vulnerabilities
- name: Scan SBOM with Grype
  run: |
    grype sbom:sbom.json --fail-on high

Evaluación del riesgo de las dependencias

No todas las dependencias implican el mismo riesgo. Un framework con 10,000 estrellas en GitHub y respaldo corporativo es muy distinto de una utilidad mantenida por una sola persona con 3 descargas por semana.

tstypescript
// Dependency risk evaluation framework
interface DependencyRisk {
  name: string;
  version: string;
  riskScore: number; // 0-100
  factors: RiskFactor[];
}
 
interface RiskFactor {
  category: string;
  description: string;
  severity: "low" | "medium" | "high" | "critical";
}
 
function evaluateDependencyRisk(
  pkg: PackageMetadata
): DependencyRisk {
  const factors: RiskFactor[] = [];
 
  // Maintainer risk
  if (pkg.maintainers.length === 1) {
    factors.push({
      category: "maintainer",
      description:
        "Single maintainer — bus factor of 1",
      severity: "medium",
    });
  }
 
  // Activity risk
  const daysSinceLastPublish = dateDiffDays(
    pkg.lastPublished,
    new Date()
  );
  if (daysSinceLastPublish > 365) {
    factors.push({
      category: "activity",
      description:
        `No updates in ${daysSinceLastPublish} days`,
      severity: "high",
    });
  }
 
  // Dependency depth risk
  if (pkg.transitiveDepCount > 100) {
    factors.push({
      category: "supply-chain",
      description:
        `${pkg.transitiveDepCount} transitive dependencies`,
      severity: "high",
    });
  }
 
  // Permission risk — postinstall scripts
  if (pkg.hasInstallScript) {
    factors.push({
      category: "permissions",
      description: "Has postinstall script",
      severity: "high",
    });
  }
 
  // Typosquatting risk
  if (pkg.weeklyDownloads < 100 && pkg.ageInDays < 30) {
    factors.push({
      category: "typosquatting",
      description:
        "New package with low downloads — potential typosquat",
      severity: "critical",
    });
  }
 
  const riskScore = calculateRiskScore(factors);
 
  return {
    name: pkg.name,
    version: pkg.version,
    riskScore,
    factors,
  };
}
 
function calculateRiskScore(
  factors: RiskFactor[]
): number {
  const weights = {
    low: 5,
    medium: 15,
    high: 30,
    critical: 50,
  };
  const total = factors.reduce(
    (sum, f) => sum + weights[f.severity],
    0
  );
  return Math.min(100, total);
}

Monitoreo automatizado de vulnerabilidades

Las vulnerabilidades se divulgan después de que ya hiciste el despliegue. El monitoreo continuo detecta nuevos CVE frente a tu árbol de dependencias actual.

ymlyaml
# Scheduled vulnerability scanning
name: Dependency Vulnerability Monitor
 
on:
  schedule:
    - cron: '0 8 * * 1-5'  # Weekdays at 8 AM
  workflow_dispatch:
 
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci --ignore-scripts
 
      - name: Scan with npm audit
        id: audit
        continue-on-error: true
        run: |
          npm audit --json > audit-results.json
          echo "vulnerabilities=$(jq '.metadata.vulnerabilities.high + .metadata.vulnerabilities.critical' audit-results.json)" >> $GITHUB_OUTPUT
 
      - name: Create alert issue
        if: steps.audit.outputs.vulnerabilities > 0
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const audit = JSON.parse(
              fs.readFileSync('audit-results.json', 'utf8')
            );
            const vulns = audit.vulnerabilities || {};
            const critical = Object.entries(vulns)
              .filter(([, v]) => 
                v.severity === 'high' || v.severity === 'critical'
              );
            
            const body = critical.map(([name, v]) =>
              `- **${name}** (${v.severity}): ${v.via?.[0]?.title || 'Unknown'}`
            ).join('\n');
            
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `🔒 ${critical.length} high/critical vulnerability(ies) found`,
              body: `## Dependency Vulnerabilities\n\n${body}\n\nRun \`npm audit\` locally for details.`,
              labels: ['security', 'dependencies']
            });

Lista blanca de scripts de instalación

En lugar de desactivar globalmente los scripts de instalación, mantén una lista blanca explícita de los paquetes autorizados a ejecutar scripts durante la instalación.

jsonjson
{
  "scripts": {
    "preinstall": "npx only-allow npm"
  },
  "overrides": {},
  "npm": {
    "ignore-scripts": true
  }
}
jsjavascript
// .npmrc — disable scripts globally, allowlist specific packages
ignore-scripts=true
 
// scripts-allow.json — explicit allowlist
{
  "allowedPackages": [
    "esbuild",
    "sharp",
    "better-sqlite3"
  ],
  "reason": {
    "esbuild": "Requires platform-specific binary download",
    "sharp": "Native image processing bindings",
    "better-sqlite3": "Native SQLite bindings"
  }
}
tstypescript
// Dependency addition review checklist
interface DependencyReviewChecklist {
  packageName: string;
  reviewer: string;
  checks: {
    question: string;
    answer: boolean;
    notes: string;
  }[];
}
 
const checklist: DependencyReviewChecklist = {
  packageName: "new-package",
  reviewer: "team-lead",
  checks: [
    {
      question: "Is this functionality available in the stdlib or existing deps?",
      answer: false,
      notes: "Checked: no equivalent in existing deps",
    },
    {
      question: "Does it have regular maintenance activity?",
      answer: true,
      notes: "Last commit 2 weeks ago, 15 releases this year",
    },
    {
      question: "Does it have postinstall scripts?",
      answer: false,
      notes: "Verified: no install scripts",
    },
    {
      question: "Is the transitive dependency count acceptable?",
      answer: true,
      notes: "3 transitive deps, all well-known",
    },
    {
      question: "Are there known vulnerabilities?",
      answer: false,
      notes: "npm audit clean, no open CVEs",
    },
  ],
};

Puntos clave

La verificación de lockfiles con npm ci --ignore-scripts en CI garantiza builds reproducibles y evita ataques mediante scripts de post-instalación, el principal vector de los compromisos de la cadena de suministro en el ecosistema npm. La generación de SBOM con CycloneDX o SPDX cataloga cada componente de tu aplicación, lo que permite responder rápidamente cuando se divulgan nuevas vulnerabilidades en las dependencias que usas. La evaluación del riesgo de las dependencias debe considerar el número de mantenedores, la frecuencia de actualización, la profundidad de las dependencias transitivas, los scripts de post-instalación y los patrones de descarga, ya que no todos los paquetes implican el mismo riesgo. El escaneo programado de vulnerabilidades con creación automática de issues detecta CVE recién divulgados en tu árbol de dependencias existente, no solo en el momento de la instalación. Una lista blanca explícita para los paquetes autorizados a ejecutar scripts de instalación es más segura que permitir o desactivar los scripts de forma global: los paquetes que necesitan binarios nativos obtienen scripts, y todos los demás se ejecutan sin ellos. Cada nueva dependencia que se agregue debe pasar por una lista de verificación que confirme que ninguna dependencia existente ofrece la misma funcionalidad, que el paquete tiene mantenimiento activo y que el impacto de sus dependencias transitivas es aceptable.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX