@secureci/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -45,6 +45,7 @@ const pipeline_contract_validator_1 = require("./parser/pipeline-contract-valida
45
45
  const report_1 = require("./report");
46
46
  const init_1 = require("./init");
47
47
  const inventory_1 = require("./inventory");
48
+ const github_lockfile_1 = require("./github-lockfile");
48
49
  const logger_1 = require("./utils/logger");
49
50
  const key_value_1 = require("./utils/key-value");
50
51
  const config_1 = require("./config");
@@ -195,7 +196,7 @@ program
195
196
  .option('--dry-run', `Print the resolved inventory as JSON without writing or checking ${LOCKFILE_NAME}`)
196
197
  .option('--update-lock', `Write/update ./${LOCKFILE_NAME} with the current commit SHAs (commit it to your repo)`)
197
198
  .option('--update-lockfile', `Alias of --update-lock`)
198
- .option('--check', `Compare the current commit SHAs against ./${LOCKFILE_NAME} and fail on drift`)
199
+ .option('--check', `Compare the current commit SHAs against ./${LOCKFILE_NAME} (or GitHub's ${github_lockfile_1.GITHUB_LOCKFILE} when that is the only lockfile) and fail on drift`)
199
200
  .option('--compare', `Alias of --check`)
200
201
  .action(async (files, options) => {
201
202
  await runInventory(files, options);
@@ -283,38 +284,58 @@ async function runInventory(files, options) {
283
284
  return;
284
285
  }
285
286
  const lockPath = path.join(process.cwd(), LOCKFILE_NAME);
286
- const lockExists = fs.existsSync(lockPath);
287
+ // secureci.lock is first-class; GitHub's actions.lock is used only when
288
+ // it is the lockfile the repository provides.
289
+ const source = (0, github_lockfile_1.findLockfile)(process.cwd());
287
290
  const writeLock = () => {
288
291
  const updated = { version: 1, actions: current };
289
292
  fs.writeFileSync(lockPath, JSON.stringify(updated, null, 2) + '\n');
290
293
  logger_1.logger.success(`Wrote ${Object.keys(current).length} action(s) to ${LOCKFILE_NAME}. Commit it to track supply-chain drift.`);
291
294
  };
292
295
  // `--update-lock`, or bare `inventory` when no lockfile exists yet: write it.
293
- if (update || (!check && !lockExists)) {
296
+ if (update || (!check && !source)) {
294
297
  writeLock();
295
298
  return;
296
299
  }
297
300
  // Otherwise compare (explicit `--check`, or bare `inventory` with a lockfile).
298
- if (!lockExists) {
299
- logger_1.logger.error(`No ${LOCKFILE_NAME} found. Create one with: secureci inventory --update-lock`);
301
+ if (!source) {
302
+ logger_1.logger.error(`No ${LOCKFILE_NAME} found (or GitHub's ${github_lockfile_1.GITHUB_LOCKFILE}). Create one with: secureci inventory --update-lock`);
300
303
  process.exit(1);
301
304
  }
302
- const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
305
+ const github = source.kind === 'github';
306
+ const lockName = github ? 'actions.lock' : LOCKFILE_NAME;
307
+ const fixHint = github
308
+ ? 'run `gh actions-lock` to update actions.lock'
309
+ : 'run with --update-lock to accept it';
310
+ let lock;
311
+ try {
312
+ const text = fs.readFileSync(source.file, 'utf-8');
313
+ lock = github ? (0, github_lockfile_1.parseGitHubLockfile)(text) : JSON.parse(text);
314
+ }
315
+ catch (error) {
316
+ logger_1.logger.error(`Could not read ${lockName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
317
+ process.exit(1);
318
+ }
319
+ logger_1.logger.info(`Checking against ${github ? github_lockfile_1.GITHUB_LOCKFILE : LOCKFILE_NAME}`);
303
320
  const diff = (0, inventory_1.diffLock)(current, lock);
304
321
  for (const d of diff.drifted) {
305
322
  logger_1.logger.error(`Drift: ${d.uses} now resolves to ${d.to.slice(0, 12)} (was ${d.from.slice(0, 12)})`);
306
323
  }
307
- diff.added.forEach((u) => logger_1.logger.warn(`New action not in ${LOCKFILE_NAME}: ${u}`));
308
- diff.removed.forEach((u) => logger_1.logger.info(`In ${LOCKFILE_NAME} but no longer used: ${u}`));
324
+ diff.added.forEach((u) => logger_1.logger.warn(`New action not in ${lockName}: ${u}`));
325
+ // actions.lock also records transitive dependencies, which SecureCI
326
+ // doesn't scan, so "no longer used" only means something for secureci.lock.
327
+ if (!github) {
328
+ diff.removed.forEach((u) => logger_1.logger.info(`In ${lockName} but no longer used: ${u}`));
329
+ }
309
330
  if (diff.drifted.length > 0) {
310
- logger_1.logger.error(`Action commits drifted from ${LOCKFILE_NAME}. Review the change, then run with --update-lock to accept it.`);
331
+ logger_1.logger.error(`Action commits drifted from ${lockName}. Review the change, then ${fixHint}.`);
311
332
  process.exit(1);
312
333
  }
313
334
  if (diff.added.length > 0) {
314
- logger_1.logger.warn(`New actions are not yet in ${LOCKFILE_NAME} (run --update-lock).`);
335
+ logger_1.logger.warn(`New actions are not yet in ${lockName} (${fixHint}).`);
315
336
  process.exit(1);
316
337
  }
317
- logger_1.logger.success(`No action drift against ${LOCKFILE_NAME}.`);
338
+ logger_1.logger.success(`No action drift against ${lockName}.`);
318
339
  }
319
340
  async function validateOneFile(filePath, contractFile, recurse, say) {
320
341
  const parsed = (0, workflow_parser_1.parseFile)(filePath);
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GITHUB_LOCKFILE = exports.SECURECI_LOCKFILE = void 0;
37
+ exports.findLockfile = findLockfile;
38
+ exports.parseGitHubLockfile = parseGitHubLockfile;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const YAML = __importStar(require("yaml"));
42
+ /** SecureCI's own lockfile, at the repository root. */
43
+ exports.SECURECI_LOCKFILE = 'secureci.lock';
44
+ /**
45
+ * GitHub's workflow dependency lockfile, written by `gh actions-lock`
46
+ * (public preview). See
47
+ * https://docs.github.com/actions/concepts/security/workflow-dependency-lockfiles
48
+ */
49
+ exports.GITHUB_LOCKFILE = path.join('.github', 'workflows', 'actions.lock');
50
+ /**
51
+ * Versions of GitHub's format this release reads. The format is pre-1.0 and
52
+ * may change, so an unknown version is an error rather than a guess.
53
+ */
54
+ const SUPPORTED_GITHUB_VERSIONS = ['v0.0.2', 'v0.0.3'];
55
+ const SHA1_COMMIT = /^sha1-([0-9a-f]{40})$/;
56
+ /**
57
+ * The lockfile to check against: `secureci.lock` when present (it stays
58
+ * first-class), otherwise GitHub's `actions.lock`, otherwise none.
59
+ */
60
+ function findLockfile(repoRoot) {
61
+ const secureci = path.join(repoRoot, exports.SECURECI_LOCKFILE);
62
+ if (fs.existsSync(secureci))
63
+ return { kind: 'secureci', file: secureci };
64
+ const github = path.join(repoRoot, exports.GITHUB_LOCKFILE);
65
+ if (fs.existsSync(github))
66
+ return { kind: 'github', file: github };
67
+ return undefined;
68
+ }
69
+ /**
70
+ * Read GitHub's `actions.lock` as a `uses -> commit` map, the same shape as
71
+ * `secureci.lock`. It includes transitive dependencies (actions that your
72
+ * actions use), which SecureCI doesn't scan.
73
+ */
74
+ function parseGitHubLockfile(text) {
75
+ const doc = YAML.parse(text);
76
+ const version = String(doc?.version ?? '');
77
+ if (!SUPPORTED_GITHUB_VERSIONS.includes(version)) {
78
+ throw new Error(`Unsupported actions.lock version "${version}" (this release reads ${SUPPORTED_GITHUB_VERSIONS.join(', ')}). Update @secureci/cli, or use ${exports.SECURECI_LOCKFILE}.`);
79
+ }
80
+ const actions = {};
81
+ for (const [uses, dep] of Object.entries(doc?.dependencies ?? {})) {
82
+ const match = SHA1_COMMIT.exec(String(dep?.commit ?? ''));
83
+ if (!match) {
84
+ throw new Error(`actions.lock entry ${uses} has no sha1 commit (got "${String(dep?.commit)}").`);
85
+ }
86
+ actions[uses] = match[1];
87
+ }
88
+ return { version: 1, actions };
89
+ }
90
+ //# sourceMappingURL=github-lockfile.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secureci/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "SecureCI: validate, check against pipeline standards, and run GitHub Actions workflows locally",
5
5
  "main": "dist/index.js",
6
6
  "bin": {