@secureci/cli 0.8.0 → 0.10.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
@@ -36,22 +36,62 @@ far fewer false positives than plain pattern-matching tools.
36
36
 
37
37
  ## Install
38
38
 
39
+ There are two ways to install it: from npm (needs Node.js 20+), or as a
40
+ [standalone binary](#standalone-binary-no-nodejs-needed) for Linux, macOS or
41
+ Windows that needs no Node.js.
42
+
39
43
  ```bash
40
44
  npm install -g @secureci/cli
41
45
  # or run without installing:
42
46
  npx @secureci/cli --help
43
47
  ```
44
48
 
45
- Node.js 20+ is required. To build from source instead:
46
- `npm install && npm run build`.
49
+ To build from source instead: `npm install && npm run build`.
47
50
 
48
51
  ### Standalone binary (no Node.js needed)
49
52
 
50
- Each [release](https://github.com/secure-ci/cli/releases) has a single-file
51
- binary for Linux (x64, arm64), macOS (Intel, Apple silicon) and Windows (x64),
52
- a `SHA256SUMS` file, and a signed build-provenance attestation. Pick the asset
53
- for your platform: `secureci-linux-x64`, `secureci-linux-arm64`,
54
- `secureci-darwin-x64`, `secureci-darwin-arm64` or `secureci-win-x64.exe`.
53
+ On macOS or Linux, the install script picks the binary for your platform,
54
+ checks it against the release's `SHA256SUMS` (a mismatch aborts), verifies its
55
+ build provenance when the [GitHub CLI](https://cli.github.com) is signed in, and
56
+ installs it to `~/.local/bin` without `sudo`:
57
+
58
+ ```bash
59
+ curl -fsSL https://secure-ci.dev/install.sh | sh
60
+ ```
61
+
62
+ To read the script before running it ([source](install.sh)):
63
+
64
+ ```bash
65
+ curl -fsSL https://secure-ci.dev/install.sh -o install.sh
66
+ less install.sh
67
+ sh install.sh
68
+ ```
69
+
70
+ Set `SECURECI_VERSION=0.10.0` to install a specific release,
71
+ `SECURECI_INSTALL_DIR` to install somewhere else, and
72
+ `SECURECI_REQUIRE_ATTESTATION=1` to fail unless `gh` verifies build provenance
73
+ (e.g. `curl -fsSL https://secure-ci.dev/install.sh | SECURECI_VERSION=0.10.0 sh`).
74
+ `secure-ci.dev/install.sh` redirects to [`install.sh`](install.sh) on this
75
+ repository's `main` branch.
76
+
77
+ Or download a binary yourself. Every [release](https://github.com/secure-ci/cli/releases/latest) has a
78
+ single-file binary for each platform below. The links always point to the
79
+ latest release:
80
+
81
+ | Platform | Download |
82
+ | --------------------- | ---------------------------------------------------------------------------------------------------------- |
83
+ | Linux x64 | [`secureci-linux-x64`](https://github.com/secure-ci/cli/releases/latest/download/secureci-linux-x64) |
84
+ | Linux arm64 | [`secureci-linux-arm64`](https://github.com/secure-ci/cli/releases/latest/download/secureci-linux-arm64) |
85
+ | macOS (Apple silicon) | [`secureci-darwin-arm64`](https://github.com/secure-ci/cli/releases/latest/download/secureci-darwin-arm64) |
86
+ | macOS (Intel) | [`secureci-darwin-x64`](https://github.com/secure-ci/cli/releases/latest/download/secureci-darwin-x64) |
87
+ | Windows x64 | [`secureci-win-x64.exe`](https://github.com/secure-ci/cli/releases/latest/download/secureci-win-x64.exe) |
88
+ | Checksums | [`SHA256SUMS`](https://github.com/secure-ci/cli/releases/latest/download/SHA256SUMS) |
89
+
90
+ Before you run a binary, check it against `SHA256SUMS`. Each binary also has a
91
+ signed build-provenance attestation, which `gh attestation verify` checks. The
92
+ commands below download, verify and install it in one go. To install a
93
+ specific version, take its assets from that version's
94
+ [release page](https://github.com/secure-ci/cli/releases).
55
95
 
56
96
  ```bash
57
97
  # macOS / Linux (here: Linux x64)
@@ -107,6 +147,17 @@ secureci inventory --check # fail on drift (use in CI)
107
147
  secureci pin
108
148
  ```
109
149
 
150
+ ### Agent skill
151
+
152
+ [`skills/secureci`](skills/secureci/SKILL.md) walks a coding agent (Claude Code,
153
+ Codex, Cursor, …) through setting SecureCI up in a repo: pin actions, commit
154
+ `secureci.lock`, and add the `secure-ci/cli` action as a step in an existing job
155
+ (replacing `actionlint` if present).
156
+
157
+ ```bash
158
+ npx skills add secure-ci/cli --skill secureci
159
+ ```
160
+
110
161
  ## Where it shines
111
162
 
112
163
  - **Fast feedback loop** — iterate on a workflow in seconds instead of
package/dist/cli.js CHANGED
@@ -278,8 +278,10 @@ initCommand
278
278
  .option('--no-upload', 'Do not upload SARIF to code scanning (keeps contents: read only)')
279
279
  .option('-o, --output <path>', 'Where to write the workflow', '.github/workflows/secureci.yml')
280
280
  .option('-f, --force', 'Overwrite the output file if it already exists')
281
- .action((options) => {
282
- runInit(options);
281
+ .option('--no-pin', 'Keep tag refs (e.g. actions/checkout@v4) instead of pinning them to commit SHAs')
282
+ .option('--offline', 'Pin only refs already in the action cache; others keep their tag')
283
+ .action(async (options) => {
284
+ await runInit(options);
283
285
  });
284
286
  function defaultWorkflowFiles() {
285
287
  const dir = path.join(process.cwd(), '.github', 'workflows');
@@ -455,7 +457,6 @@ async function runInventory(files, options) {
455
457
  }
456
458
  // --dry-run: print the resolved inventory as JSON, touching nothing.
457
459
  if (options.dryRun) {
458
- // eslint-disable-next-line no-console
459
460
  console.log(JSON.stringify({
460
461
  actions,
461
462
  advisories: advisoryReport.findings,
@@ -788,7 +789,6 @@ async function validateFiles(files, contractFile, recurse, format, offline) {
788
789
  say(() => logger_1.logger.info(line.text));
789
790
  }
790
791
  if (quiet) {
791
- // eslint-disable-next-line no-console
792
792
  console.log((0, report_1.formatReport)(entries, format, { contract }));
793
793
  if (!allValid)
794
794
  process.exit(1);
@@ -810,22 +810,36 @@ async function validateFiles(files, contractFile, recurse, format, offline) {
810
810
  if (!allValid)
811
811
  process.exit(1);
812
812
  }
813
- function runInit(options) {
813
+ async function runInit(options) {
814
814
  const output = path.resolve(process.cwd(), options.output);
815
815
  if (fs.existsSync(output) && !options.force) {
816
816
  logger_1.logger.error(`${options.output} already exists; pass --force to overwrite it.`);
817
817
  process.exit(1);
818
818
  }
819
- const content = (0, init_1.renderInitWorkflow)({
819
+ let unpinned = [];
820
+ let content = (0, init_1.renderInitWorkflow)({
820
821
  version: (0, version_1.readVersion)(),
821
822
  contract: options.contract,
822
823
  upload: options.upload
823
824
  });
825
+ // Pin action tags to commit SHAs (`owner/repo@<sha> # v4`), as
826
+ // `secureci pin` would. A ref that can't be resolved keeps its tag.
827
+ if (options.pin) {
828
+ const result = await (0, pin_1.pinContent)(content, (uses) => options.offline
829
+ ? Promise.resolve((0, action_fetcher_1.cachedActionSha)(uses))
830
+ : (0, action_fetcher_1.resolveActionSha)(uses));
831
+ content = result.content;
832
+ unpinned = result.unresolved;
833
+ }
824
834
  fs.mkdirSync(path.dirname(output), { recursive: true });
825
835
  fs.writeFileSync(output, content);
826
836
  logger_1.logger.success(`Wrote ${options.output}`);
837
+ for (const problem of unpinned) {
838
+ logger_1.logger.warn(`${problem.uses} ${problem.reason}; left on its tag. Run \`secureci pin ${options.output}\` to pin it later.`);
839
+ }
827
840
  if (options.upload) {
828
- logger_1.logger.info('The workflow uploads SARIF to code scanning (needs security-events: write).');
841
+ logger_1.logger.info('The workflow uploads SARIF to code scanning (needs actions: read and security-events: write).');
842
+ logger_1.logger.info('Code scanning must be enabled for the repository (Settings > Code security) for the SARIF upload to work; otherwise regenerate with --no-upload.');
829
843
  }
830
844
  logger_1.logger.info('Commit it and open a pull request to see SecureCI run.');
831
845
  }
package/dist/init.js CHANGED
@@ -8,7 +8,8 @@ exports.renderInitWorkflow = renderInitWorkflow;
8
8
  *
9
9
  * The output is deterministic for a given set of options, so it can be
10
10
  * regenerated and diffed. With uploads on (the default) the job asks for
11
- * `security-events: write` and lets the action upload SARIF; with uploads off
11
+ * `actions: read` and `security-events: write` and lets the action upload
12
+ * SARIF; with uploads off
12
13
  * it keeps `contents: read` only and passes `upload-sarif: false`.
13
14
  */
14
15
  function renderInitWorkflow(options) {
@@ -31,8 +32,11 @@ function renderInitWorkflow(options) {
31
32
  ];
32
33
  // Write scopes belong on the job, not the workflow. Job-level permissions
33
34
  // replace the top-level ones, so contents: read is repeated for checkout.
35
+ // The SARIF upload (github/codeql-action/upload-sarif) needs actions: read
36
+ // to read the workflow run (required on private repositories) and
37
+ // security-events: write to upload the results.
34
38
  if (upload) {
35
- lines.push(' permissions:', ' contents: read', ' security-events: write');
39
+ lines.push(' # The SARIF upload needs code scanning enabled for this repository', ' # (Settings > Code security; on private repositories this needs', ' # GitHub Code Security / Advanced Security). Without it the upload', ' # step fails: enable it, or give the secure-ci/cli step', ' # `with: upload-sarif: false` (and drop the two scopes below).', ' permissions:', ' contents: read', ' actions: read # upload-sarif reads the workflow run', ' security-events: write # upload SARIF to code scanning');
36
40
  }
37
41
  lines.push(' steps:', ' - uses: actions/checkout@v4', ` - uses: secure-ci/cli@v${options.version}`);
38
42
  const withLines = [];
@@ -66,7 +66,7 @@ function parsePipelineContract(filePath) {
66
66
  parsed = YAML.parse(content);
67
67
  }
68
68
  catch (error) {
69
- throw new Error(`Failed to parse contract file: ${error instanceof Error ? error.message : 'Unknown error'}`);
69
+ throw new Error(`Failed to parse contract file: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
70
70
  }
71
71
  if (parsed === null || parsed === undefined) {
72
72
  return {};
@@ -62,7 +62,9 @@ function parseWorkflow(filePath) {
62
62
  }
63
63
  catch (error) {
64
64
  if (error instanceof Error) {
65
- throw new Error(`Failed to parse workflow: ${error.message}`);
65
+ throw new Error(`Failed to parse workflow: ${error.message}`, {
66
+ cause: error
67
+ });
66
68
  }
67
69
  throw error;
68
70
  }
@@ -94,7 +96,9 @@ function parseAction(filePath) {
94
96
  }
95
97
  catch (error) {
96
98
  if (error instanceof Error) {
97
- throw new Error(`Failed to parse action: ${error.message}`);
99
+ throw new Error(`Failed to parse action: ${error.message}`, {
100
+ cause: error
101
+ });
98
102
  }
99
103
  throw error;
100
104
  }
@@ -452,7 +452,7 @@ function findCache(scope, version, key, restoreKeys) {
452
452
  if (fs.existsSync(path.join(exact, 'meta.json'))) {
453
453
  return { dir: exact, key, created: 0 };
454
454
  }
455
- let entries = [];
455
+ let entries;
456
456
  try {
457
457
  entries = fs
458
458
  .readdirSync(scope)
@@ -225,7 +225,7 @@ function loadEvent(options, inputs, on) {
225
225
  payload = JSON.parse(fs.readFileSync(options.eventPath, 'utf-8'));
226
226
  }
227
227
  catch (error) {
228
- throw new Error(`Invalid event payload ${options.eventPath}: ${error instanceof Error ? error.message : String(error)}`);
228
+ throw new Error(`Invalid event payload ${options.eventPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
229
229
  }
230
230
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
231
231
  throw new Error(`Invalid event payload ${options.eventPath}: expected a JSON object`);
@@ -280,7 +280,7 @@ function runPreIf(action, step, ctx) {
280
280
  return (0, expression_1.evaluateCondition)(action.runs['pre-if'] ?? 'always()', ctx);
281
281
  }
282
282
  catch (error) {
283
- throw new Error((0, expression_1.ifEvaluationError)(`step 'Pre ${step.name || step.uses}'`, error));
283
+ throw new Error((0, expression_1.ifEvaluationError)(`step 'Pre ${step.name || step.uses}'`, error), { cause: error });
284
284
  }
285
285
  }
286
286
  //# sourceMappingURL=node-action.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secureci/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.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": {
@@ -19,7 +19,7 @@
19
19
  "test:watch": "vitest",
20
20
  "test:coverage": "vitest run --coverage",
21
21
  "dev": "tsc --watch",
22
- "lint": "eslint src --ext .ts",
22
+ "lint": "eslint src",
23
23
  "format": "prettier --write .",
24
24
  "prepare": "npm run build"
25
25
  },
@@ -63,18 +63,20 @@
63
63
  "yaml": "^2.3.4"
64
64
  },
65
65
  "devDependencies": {
66
- "@jabraf/dev": "^0.1.4",
66
+ "@eslint/js": "^10.0.1",
67
67
  "@jabraf/prettier": "^3.0.1",
68
68
  "@types/node": "^20.10.6",
69
- "@typescript-eslint/eslint-plugin": "^8.71.1",
70
- "@typescript-eslint/parser": "^8.71.1",
71
69
  "@vitest/coverage-v8": "^4.1.12",
72
- "eslint": "^8.56.0",
70
+ "eslint": "^10.14.0",
73
71
  "postject": "1.0.0-alpha.6",
74
72
  "typescript": "^5.3.3",
73
+ "typescript-eslint": "^8.72.1",
75
74
  "vitest": "^4.1.12"
76
75
  },
77
76
  "volta": {
78
77
  "node": "24.0.0"
78
+ },
79
+ "overrides": {
80
+ "minimatch": "^11.0.4"
79
81
  }
80
82
  }