File size: 4,080 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env node
// scripts/check/check-lockfile.mjs
// Gate de política de lockfile (CLAUDE.md — extensão Hard Rule #1).
//
// Objetivo: detectar supply-chain poisoning no package-lock.json antes que código
// malicioso entre no repo. Verifica:
//   --validate-https       → toda URL "resolved" deve usar HTTPS (bloqueia http://)
//   --validate-integrity   → todo pacote deve ter hash de integridade sha512
//   --allowed-hosts npm    → apenas registry.npmjs.org é host permitido
//
// Complementa check-deps (Fase 2 / allowlist de nomes): aquele garante que só
// nomes aprovados entram; este garante que os pacotes instalados vieram do registry
// legítimo com integridade verificável.
//
// Referência: PLANO-QUALITY-GATES-FASE7.md, Task 7.7.
// Tool: lockfile-lint v5 (node_modules/.bin/lockfile-lint).

import { execFileSync } from "node:child_process";
import path from "node:path";
import fs from "node:fs";
import { pathToFileURL } from "node:url";

const ROOT = process.cwd();

/**
 * Returns the canonical lockfile-lint configuration used by this gate.
 * Exporting this object makes the policy auditable and unit-testable without
 * spawning a child process.
 *
 * @returns {{
 *   lockfilePath: string,
 *   type: string,
 *   validateHttps: boolean,
 *   validateIntegrity: boolean,
 *   allowedHosts: string[],
 * }}
 */
export function getLockfileLintConfig() {
  return {
    lockfilePath: path.join(ROOT, "package-lock.json"),
    type: "npm",
    validateHttps: true,
    validateIntegrity: true,
    // Only the official npm registry is permitted.
    // registry.npmjs.org resolves to the "npm" shorthand in lockfile-lint.
    // If the project ever adopts a scoped/private registry, add its hostname here
    // and document the justification.
    allowedHosts: ["npm"],
  };
}

/**
 * Builds the argv array to pass to the lockfile-lint binary, derived from
 * the config returned by getLockfileLintConfig().
 *
 * @param {ReturnType<typeof getLockfileLintConfig>} cfg
 * @returns {string[]}
 */
export function buildLockfileLintArgs(cfg) {
  const args = [
    "--path", cfg.lockfilePath,
    "--type", cfg.type,
  ];
  if (cfg.validateHttps) args.push("--validate-https");
  if (cfg.validateIntegrity) args.push("--validate-integrity");
  if (cfg.allowedHosts.length) {
    args.push("--allowed-hosts", ...cfg.allowedHosts);
  }
  return args;
}

function main() {
  const cfg = getLockfileLintConfig();

  if (!fs.existsSync(cfg.lockfilePath)) {
    console.error(
      `[check-lockfile] FAIL — lockfile not found: ${cfg.lockfilePath}\n` +
        "  → Run `npm install` to generate package-lock.json"
    );
    process.exit(1);
  }

  const bin = path.join(ROOT, "node_modules", ".bin", "lockfile-lint");
  if (!fs.existsSync(bin)) {
    console.error(
      `[check-lockfile] FAIL — lockfile-lint binary not found at:\n  ${bin}\n` +
        "  → Run `npm install` to install dev dependencies"
    );
    process.exit(1);
  }

  const args = buildLockfileLintArgs(cfg);

  try {
    const output = execFileSync(bin, args, { encoding: "utf8" });
    // lockfile-lint outputs a green ✔ message on success
    console.log("[check-lockfile] OK —", output.trim());
  } catch (err) {
    const stdout = err.stdout ?? "";
    const stderr = err.stderr ?? "";
    console.error("[check-lockfile] FAIL — lockfile-lint found policy violations:");
    if (stdout) console.error(stdout);
    if (stderr) console.error(stderr);
    console.error(
      "\n  Possible causes:\n" +
        "  • A package was resolved from a non-HTTPS URL (http:// poisoning attempt)\n" +
        "  • A package is missing its integrity hash (tampered or legacy entry)\n" +
        "  • A package was resolved from a host other than registry.npmjs.org\n" +
        "    If a scoped/private registry is intentionally used, add its hostname\n" +
        "    to getLockfileLintConfig().allowedHosts in scripts/check/check-lockfile.mjs"
    );
    process.exit(1);
  }
}

if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();