@secureci/cli 0.1.1 → 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/README.md CHANGED
@@ -7,7 +7,7 @@ Test and vet your GitHub Actions **before you push**.
7
7
  `@secureci/cli` (the `secureci` command) runs your workflows locally, validates
8
8
  them, and checks them against your organization's pipeline standards — without
9
9
  committing, pushing, or waiting on a CI runner. It is the free, open-source part
10
- of [SecureCI](https://www.secure-ci.dev).
10
+ of [SecureCI](https://secure-ci.dev).
11
11
 
12
12
  > 📖 This README is the quick tour. Every flag, context and edge case lives in
13
13
  > the **[full reference → DOCS.md](DOCS.md)**.
@@ -38,8 +38,8 @@ npm install -g @secureci/cli
38
38
  npx @secureci/cli --help
39
39
  ```
40
40
 
41
- Node.js 20+ is required. (Until the package is published to npm, build from
42
- source: `npm install && npm run build`.)
41
+ Node.js 20+ is required. To build from source instead:
42
+ `npm install && npm run build`.
43
43
 
44
44
  ## First run
45
45
 
@@ -74,6 +74,11 @@ secureci inventory --check # fail on drift (use in CI)
74
74
  check.
75
75
  - **Machine-readable output** — `--format json|sarif` drops straight into CI and
76
76
  GitHub code scanning.
77
+ - **Audit evidence** — the contract, `secureci.lock` and reports are plain files
78
+ you can commit and keep. See
79
+ [using output as audit evidence](DOCS.md#using-output-as-audit-evidence) and
80
+ the [compliance mapping](https://secure-ci.dev/compliance) (PCI DSS, DORA,
81
+ NIS2, ISO 27001, SOC 2 and more).
77
82
 
78
83
  See the [reference](DOCS.md) for the complete command list and flags.
79
84
 
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);
@@ -413,6 +434,7 @@ function runInit(options) {
413
434
  process.exit(1);
414
435
  }
415
436
  const content = (0, init_1.renderInitWorkflow)({
437
+ version: readVersion(),
416
438
  contract: options.contract,
417
439
  upload: options.upload
418
440
  });
@@ -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/dist/init.js CHANGED
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderInitWorkflow = renderInitWorkflow;
4
4
  /**
5
5
  * Render a ready-to-commit `.github/workflows/secureci.yml` that runs SecureCI
6
- * on every pull request via the published `secure-ci/cli` action.
6
+ * on every pull request via the published `secure-ci/cli` action, pinned to
7
+ * the release tag matching this CLI's version.
7
8
  *
8
9
  * The output is deterministic for a given set of options, so it can be
9
10
  * regenerated and diffed. With uploads on (the default) the workflow asks for
@@ -26,7 +27,7 @@ function renderInitWorkflow(options) {
26
27
  ];
27
28
  if (upload)
28
29
  lines.push(' security-events: write');
29
- lines.push('', 'jobs:', ' secureci:', ' runs-on: ubuntu-latest', ' steps:', ' - uses: actions/checkout@v4', ' - uses: secure-ci/cli@v1');
30
+ lines.push('', 'jobs:', ' secureci:', ' runs-on: ubuntu-latest', ' steps:', ' - uses: actions/checkout@v4', ` - uses: secure-ci/cli@v${options.version}`);
30
31
  const withLines = [];
31
32
  if (contract)
32
33
  withLines.push(` contract: ${yamlScalar(contract)}`);
package/dist/report.js CHANGED
@@ -59,7 +59,7 @@ function formatReport(entries, format) {
59
59
  tool: {
60
60
  driver: {
61
61
  name: 'secureci',
62
- informationUri: 'https://www.secure-ci.dev',
62
+ informationUri: 'https://secure-ci.dev',
63
63
  rules: Object.entries(RULES).map(([id, name]) => ({ id, name }))
64
64
  }
65
65
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secureci/cli",
3
- "version": "0.1.1",
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": {
@@ -42,7 +42,7 @@
42
42
  }
43
43
  ],
44
44
  "license": "MIT",
45
- "homepage": "https://www.secure-ci.dev",
45
+ "homepage": "https://secure-ci.dev",
46
46
  "repository": {
47
47
  "type": "git",
48
48
  "url": "git+https://github.com/secure-ci/cli.git"