@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 +58 -7
- package/dist/cli.js +21 -7
- package/dist/init.js +6 -2
- package/dist/parser/pipeline-contract-parser.js +1 -1
- package/dist/parser/workflow-parser.js +6 -2
- package/dist/runner/builtin-actions.js +1 -1
- package/dist/runner/github-context.js +1 -1
- package/dist/runner/node-action.js +1 -1
- package/package.json +8 -6
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
.
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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.
|
|
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
|
|
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
|
-
"@
|
|
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": "^
|
|
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
|
}
|