@secureci/cli 0.9.0 → 1.0.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 -5
- package/dist/init.js +6 -2
- package/dist/runner/builtin-actions.js +1 -1
- package/dist/runner/docker-action.js +1 -1
- package/dist/runner/job-runtime.js +1 -1
- package/package.json +1 -1
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=1.0.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=1.0.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');
|
|
@@ -808,22 +810,36 @@ async function validateFiles(files, contractFile, recurse, format, offline) {
|
|
|
808
810
|
if (!allValid)
|
|
809
811
|
process.exit(1);
|
|
810
812
|
}
|
|
811
|
-
function runInit(options) {
|
|
813
|
+
async function runInit(options) {
|
|
812
814
|
const output = path.resolve(process.cwd(), options.output);
|
|
813
815
|
if (fs.existsSync(output) && !options.force) {
|
|
814
816
|
logger_1.logger.error(`${options.output} already exists; pass --force to overwrite it.`);
|
|
815
817
|
process.exit(1);
|
|
816
818
|
}
|
|
817
|
-
|
|
819
|
+
let unpinned = [];
|
|
820
|
+
let content = (0, init_1.renderInitWorkflow)({
|
|
818
821
|
version: (0, version_1.readVersion)(),
|
|
819
822
|
contract: options.contract,
|
|
820
823
|
upload: options.upload
|
|
821
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
|
+
}
|
|
822
834
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
823
835
|
fs.writeFileSync(output, content);
|
|
824
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
|
+
}
|
|
825
840
|
if (options.upload) {
|
|
826
|
-
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.');
|
|
827
843
|
}
|
|
828
844
|
logger_1.logger.info('Commit it and open a pull request to see SecureCI run.');
|
|
829
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 = [];
|
|
@@ -391,7 +391,7 @@ async function uploadArtifact(fsys, inputs, runId) {
|
|
|
391
391
|
copyFile(file.host, path.join(target, ...rel.split('/')));
|
|
392
392
|
digest.update(rel).update(fs.readFileSync(file.host));
|
|
393
393
|
}
|
|
394
|
-
const id = String(parseInt(crypto.createHash('
|
|
394
|
+
const id = String(parseInt(crypto.createHash('sha256').update(target).digest('hex').slice(0, 8), 16));
|
|
395
395
|
return ok(`Uploaded ${files.length} file(s) as artifact "${name}" to ${target}`, {
|
|
396
396
|
'artifact-id': id,
|
|
397
397
|
'artifact-url': '',
|
|
@@ -90,7 +90,7 @@ async function executeDockerAction(action, actionPath, step, actionInputs, calle
|
|
|
90
90
|
duration: Date.now() - startTime
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
const tag = `secureci-action-${crypto.createHash('
|
|
93
|
+
const tag = `secureci-action-${crypto.createHash('sha256').update(actionDir).digest('hex').slice(0, 12)}`;
|
|
94
94
|
logger_1.logger.info(`Building Docker action image: ${action.name} (${tag})`);
|
|
95
95
|
const buildResult = await containerRuntime.build(actionDir, tag, dockerfileRelative);
|
|
96
96
|
if (!buildResult.success) {
|
|
@@ -285,7 +285,7 @@ function actionDirInContainer(container, actionDir) {
|
|
|
285
285
|
let containerDir = container.actionDirs.get(actionDir);
|
|
286
286
|
if (!containerDir) {
|
|
287
287
|
const hash = crypto
|
|
288
|
-
.createHash('
|
|
288
|
+
.createHash('sha256')
|
|
289
289
|
.update(actionDir)
|
|
290
290
|
.digest('hex')
|
|
291
291
|
.slice(0, 12);
|