@secureci/cli 1.0.0 → 1.1.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 +36 -15
- package/dist/advisories.js +33 -6
- package/dist/cli.js +103 -4
- package/dist/config.js +12 -1
- package/dist/inventory.js +52 -0
- package/dist/org-scan-script.js +6 -0
- package/dist/org-scan.js +88 -0
- package/dist/parser/pipeline-contract-validator.js +56 -25
- package/dist/runner/action-fetcher.js +6 -1
- package/dist/runner/actions.js +7 -7
- package/dist/runner/checkout.js +124 -8
- package/dist/runner/container-policy.js +32 -0
- package/dist/runner/docker-action.js +1 -1
- package/dist/runner/executor.js +28 -6
- package/dist/runner/expression.js +125 -3
- package/dist/runner/job-container.js +57 -11
- package/dist/runner/job-runtime.js +4 -3
- package/dist/runner/node-action.js +1 -1
- package/dist/runner/runner-env.js +20 -0
- package/dist/runner/steps.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@ 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
10
|
of [SecureCI](https://secure-ci.dev).
|
|
11
11
|
|
|
12
|
+
<a href="https://secure-ci.dev/assets/video/secureci-walkthrough-16x9.mp4"><img src="https://secure-ci.dev/assets/readme-demo.svg" width="848" alt="Terminal demo of the secureci CLI on a demo repository: run executes a workflow locally and its tests pass; validate fails a deploy workflow against a pipeline contract (security scan below the minimum version, wrong step order, actions not pinned to a commit SHA); pin pins actions/checkout@v4 to its full commit SHA, keeping # v4 for Dependabot; inventory --check fails because the v4 tag now resolves to a different commit than the one locked."></a>
|
|
13
|
+
|
|
14
|
+
<sub>Real output from the CLI on a demo repository.</sub>
|
|
15
|
+
|
|
16
|
+
▶️ **[Watch the 60-second walkthrough](https://secure-ci.dev/assets/video/secureci-walkthrough-16x9.mp4)**
|
|
17
|
+
|
|
12
18
|
> 📖 This README is the quick tour. Every flag, context and edge case lives in
|
|
13
19
|
> the **[full reference → DOCS.md](DOCS.md)**.
|
|
14
20
|
|
|
@@ -67,10 +73,13 @@ less install.sh
|
|
|
67
73
|
sh install.sh
|
|
68
74
|
```
|
|
69
75
|
|
|
70
|
-
Set `SECURECI_VERSION=1.
|
|
76
|
+
Set `SECURECI_VERSION=1.1.0` to install a specific release,
|
|
71
77
|
`SECURECI_INSTALL_DIR` to install somewhere else, and
|
|
72
78
|
`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.
|
|
79
|
+
(e.g. `curl -fsSL https://secure-ci.dev/install.sh | SECURECI_VERSION=1.1.0 sh`).
|
|
80
|
+
If the release's download link fails (GitHub has returned `504` for one while
|
|
81
|
+
its API still served the files), the script fetches the same files through the
|
|
82
|
+
GitHub API instead; the `SHA256SUMS` and provenance checks are unchanged.
|
|
74
83
|
`secure-ci.dev/install.sh` redirects to [`install.sh`](install.sh) on this
|
|
75
84
|
repository's `main` branch.
|
|
76
85
|
|
|
@@ -145,14 +154,20 @@ secureci inventory --check # fail on drift (use in CI)
|
|
|
145
154
|
|
|
146
155
|
# Pin action tags to commit SHAs in place (keeps `# v4` for Dependabot/Renovate)
|
|
147
156
|
secureci pin
|
|
157
|
+
|
|
158
|
+
# Scan every repository in an organisation (JSON results, never source code)
|
|
159
|
+
secureci org scan my-org
|
|
148
160
|
```
|
|
149
161
|
|
|
150
162
|
### Agent skill
|
|
151
163
|
|
|
152
|
-
[`skills/secureci`](skills/secureci/SKILL.md)
|
|
153
|
-
Codex, Cursor, …)
|
|
154
|
-
`secureci
|
|
155
|
-
|
|
164
|
+
[`skills/secureci`](skills/secureci/SKILL.md) lets a coding agent (Claude Code,
|
|
165
|
+
Codex, Cursor, …) check every workflow it creates or edits (`secureci validate`,
|
|
166
|
+
`secureci pin --check`), run workflows locally (`secureci run`), and set
|
|
167
|
+
SecureCI up in a repo: pin actions, commit `secureci.lock`, and add the
|
|
168
|
+
`secure-ci/cli` action as a step in an existing job (replacing `actionlint` if
|
|
169
|
+
present). It also picks up requests that name actionlint, zizmor, nektos/act,
|
|
170
|
+
pinact and similar tools.
|
|
156
171
|
|
|
157
172
|
```bash
|
|
158
173
|
npx skills add secure-ci/cli --skill secureci
|
|
@@ -242,18 +257,24 @@ things to know:
|
|
|
242
257
|
|
|
243
258
|
- **Linux containers only.** `windows-*` / `macos-*` jobs run in a Linux image
|
|
244
259
|
with a warning.
|
|
245
|
-
- **Jobs running in parallel share your working copy
|
|
246
|
-
container, but every job mounts the same directory, so jobs that
|
|
247
|
-
same files (e.g. `node_modules`) can collide;
|
|
248
|
-
|
|
260
|
+
- **Jobs running in parallel share your working copy** by default. Each job has
|
|
261
|
+
its own container, but every job mounts the same directory, so jobs that
|
|
262
|
+
write the same files (e.g. `node_modules`) can collide; pass
|
|
263
|
+
`--isolate-workspace` to give each job its own copy, or `--parallel 1` to run
|
|
264
|
+
jobs one after another.
|
|
249
265
|
- **`actions/checkout` of the current repo doesn't clone** — it uses your working
|
|
250
|
-
copy as-is (so uncommitted changes are visible)
|
|
251
|
-
|
|
266
|
+
copy as-is (so uncommitted changes are visible). A `ref:` your local
|
|
267
|
+
repository has is checked out only into a `path:`; other refs and
|
|
268
|
+
`fetch-depth:` don't change what you get. Other repositories check out for
|
|
269
|
+
real and need a token.
|
|
252
270
|
- **Artifacts and cache are local stand-ins**, stored under `~/.secureci/`;
|
|
253
271
|
nothing is uploaded to or downloaded from GitHub.
|
|
254
|
-
- **
|
|
255
|
-
|
|
256
|
-
|
|
272
|
+
- **Not emulated:** `concurrency:` (noted at run start), deployment
|
|
273
|
+
`environment:` (protection rules and environment-scoped secrets/vars: pass
|
|
274
|
+
them with `--secret`/`--var`), and OIDC tokens for `id-token: write`.
|
|
275
|
+
- **Contract checks** decide `if:` conditions only from the workflow's triggers
|
|
276
|
+
(`github.event_name`) and, without `--contract-recurse all`, don't look inside
|
|
277
|
+
_remote_ composite actions / reusable workflows.
|
|
257
278
|
|
|
258
279
|
The exact scope of each is documented in
|
|
259
280
|
[DOCS.md](DOCS.md#contract-limitations-v1-scope).
|
package/dist/advisories.js
CHANGED
|
@@ -61,6 +61,17 @@ exports.VULNERABLE_ACTION_RULE = 'secureci/vulnerable-action';
|
|
|
61
61
|
/** Cached advisories are re-fetched after this long. */
|
|
62
62
|
exports.ADVISORY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
63
63
|
const ADVISORY_TIMEOUT_MS = 15000;
|
|
64
|
+
/** Pages of advisories read per repository (100 each). */
|
|
65
|
+
const MAX_ADVISORY_PAGES = 10;
|
|
66
|
+
/** The `rel="next"` URL of a `Link` header, if any. */
|
|
67
|
+
function nextPageUrl(link) {
|
|
68
|
+
for (const part of (link ?? '').split(',')) {
|
|
69
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="?next"?/);
|
|
70
|
+
if (match)
|
|
71
|
+
return match[1];
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
64
75
|
const VERSION = /^v?(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
65
76
|
/** `v1.2.3`, `1.2`, `v4`, `2.0.0-rc.1`; null for anything else. */
|
|
66
77
|
function parseVersion(text) {
|
|
@@ -359,7 +370,7 @@ function describeError(error, authenticated) {
|
|
|
359
370
|
* The advisories naming `owner/repo` in the actions ecosystem, cached per
|
|
360
371
|
* repository for `ttlMs` (default 24h). Offline, the cache is used at any
|
|
361
372
|
* age. If the API fails, a stale cache is used when there is one.
|
|
362
|
-
*
|
|
373
|
+
* Pages are followed (on the API's own host) up to 1,000 advisories.
|
|
363
374
|
*/
|
|
364
375
|
async function fetchRepoAdvisories(owner, repo, options = {}) {
|
|
365
376
|
if (!validName(owner) || !validName(repo))
|
|
@@ -386,7 +397,10 @@ async function fetchRepoAdvisories(owner, repo, options = {}) {
|
|
|
386
397
|
: { error: 'offline and not cached' };
|
|
387
398
|
}
|
|
388
399
|
const httpGet = options.httpGet ??
|
|
389
|
-
((url, headers) => (0, action_fetcher_1.
|
|
400
|
+
((url, headers) => (0, action_fetcher_1.getResponse)(url, headers, ADVISORY_TIMEOUT_MS).then((r) => ({
|
|
401
|
+
body: r.body,
|
|
402
|
+
link: [r.headers.link].flat()[0]
|
|
403
|
+
})));
|
|
390
404
|
const query = new URLSearchParams({
|
|
391
405
|
ecosystem: 'actions',
|
|
392
406
|
affects: `${owner}/${repo}`,
|
|
@@ -400,19 +414,32 @@ async function fetchRepoAdvisories(owner, repo, options = {}) {
|
|
|
400
414
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
401
415
|
});
|
|
402
416
|
let token = options.token === undefined ? (0, action_fetcher_1.githubToken)() : options.token;
|
|
403
|
-
|
|
417
|
+
const page = async (pageUrl) => {
|
|
418
|
+
const response = await httpGet(pageUrl, headers(token));
|
|
419
|
+
return typeof response === 'string' ? { body: response } : response;
|
|
420
|
+
};
|
|
404
421
|
try {
|
|
422
|
+
let response;
|
|
405
423
|
try {
|
|
406
|
-
|
|
424
|
+
response = await page(url);
|
|
407
425
|
}
|
|
408
426
|
catch (error) {
|
|
409
427
|
// A rejected token shouldn't block public advisories.
|
|
410
428
|
if (!token || error.status !== 401)
|
|
411
429
|
throw error;
|
|
412
430
|
token = null;
|
|
413
|
-
|
|
431
|
+
response = await page(url);
|
|
432
|
+
}
|
|
433
|
+
const advisories = parseAdvisories(JSON.parse(response.body));
|
|
434
|
+
// Later pages only from the API's own origin, which the token is for.
|
|
435
|
+
const origin = new URL(url).origin;
|
|
436
|
+
for (let pages = 1; pages < MAX_ADVISORY_PAGES; pages++) {
|
|
437
|
+
const next = nextPageUrl(response.link);
|
|
438
|
+
if (!next || new URL(next, url).origin !== origin)
|
|
439
|
+
break;
|
|
440
|
+
response = await page(new URL(next, url).toString());
|
|
441
|
+
advisories.push(...parseAdvisories(JSON.parse(response.body)));
|
|
414
442
|
}
|
|
415
|
-
const advisories = parseAdvisories(JSON.parse(body));
|
|
416
443
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
417
444
|
const entry = { api, fetchedAt: now, advisories };
|
|
418
445
|
fs.writeFileSync(file, JSON.stringify(entry, null, 2));
|
package/dist/cli.js
CHANGED
|
@@ -56,6 +56,11 @@ const env_file_1 = require("./utils/env-file");
|
|
|
56
56
|
const config_1 = require("./config");
|
|
57
57
|
const scheduler_1 = require("./runner/scheduler");
|
|
58
58
|
const version_1 = require("./version");
|
|
59
|
+
const org_scan_1 = require("./org-scan");
|
|
60
|
+
const org_scan_script_1 = require("./org-scan-script");
|
|
61
|
+
const node_binary_1 = require("./runner/node-binary");
|
|
62
|
+
const child_process_1 = require("child_process");
|
|
63
|
+
const os = __importStar(require("os"));
|
|
59
64
|
const fs = __importStar(require("fs"));
|
|
60
65
|
const path = __importStar(require("path"));
|
|
61
66
|
/** Committed at the repo root, like package-lock.json / yarn.lock. */
|
|
@@ -128,6 +133,8 @@ program
|
|
|
128
133
|
.option('--container-memory <limit>', 'Limit job container memory, e.g. 512m. Config: container-memory', globalConfig['container-memory'])
|
|
129
134
|
.option('--no-network', 'Turn off container network access (on by default)')
|
|
130
135
|
.option('--allow-network', 'Turn on container network access, overriding allow-network: false in the config')
|
|
136
|
+
.option('--isolate-workspace', "Run each job in its own copy of the working copy, removed when the job ends, so parallel jobs can't overwrite each other's files. Config: isolate-workspace")
|
|
137
|
+
.option('--no-isolate-workspace', 'Share the working copy between jobs, overriding isolate-workspace: true in the config')
|
|
131
138
|
.option('--parallel <n>', 'Run up to this many jobs (and matrix combinations) at once; 1 runs them one after another. Default: the number of CPUs. Config: parallel', String(defaultParallel))
|
|
132
139
|
.action(async (file, options) => {
|
|
133
140
|
if (configError) {
|
|
@@ -206,7 +213,8 @@ program
|
|
|
206
213
|
: undefined,
|
|
207
214
|
containerMemoryLimit: options.containerMemory,
|
|
208
215
|
networkDisabled: (0, config_1.resolveNetworkDisabled)({ network: options.network, allowNetwork: options.allowNetwork }, globalConfig),
|
|
209
|
-
parallel
|
|
216
|
+
parallel,
|
|
217
|
+
isolateWorkspace: (0, config_1.resolveIsolateWorkspace)(options.isolateWorkspace, globalConfig)
|
|
210
218
|
});
|
|
211
219
|
if (!result.success) {
|
|
212
220
|
process.exit(1);
|
|
@@ -292,6 +300,36 @@ function defaultWorkflowFiles() {
|
|
|
292
300
|
.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
293
301
|
.map((f) => path.join('.github', 'workflows', f));
|
|
294
302
|
}
|
|
303
|
+
/** The workflows in .github/workflows that aren't among `targets`. */
|
|
304
|
+
function otherWorkflowFiles(targets) {
|
|
305
|
+
const given = new Set(targets.map((file) => path.resolve(file)));
|
|
306
|
+
return defaultWorkflowFiles().filter((file) => !given.has(path.resolve(file)));
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The remote references these workflows use, read locally (no network), or
|
|
310
|
+
* undefined, with a warning, when one of them can't be read.
|
|
311
|
+
*/
|
|
312
|
+
async function workflowUses(files, lockName) {
|
|
313
|
+
const uses = new Set();
|
|
314
|
+
for (const file of files) {
|
|
315
|
+
try {
|
|
316
|
+
const parsed = (0, workflow_parser_1.parseFile)(file);
|
|
317
|
+
if (parsed.type !== 'workflow')
|
|
318
|
+
continue;
|
|
319
|
+
const inv = await (0, inventory_1.buildInventory)({
|
|
320
|
+
type: 'workflow',
|
|
321
|
+
path: file,
|
|
322
|
+
file: parsed.file
|
|
323
|
+
});
|
|
324
|
+
inv.actions.forEach((action) => uses.add(action.uses));
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
logger_1.logger.warn(`Could not read ${file} (${error instanceof Error ? error.message : 'Unknown error'}), so every entry in ${lockName} is kept.`);
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return uses;
|
|
332
|
+
}
|
|
295
333
|
async function runInventory(files, options) {
|
|
296
334
|
const update = options.updateLock || options.updateLockfile;
|
|
297
335
|
const check = options.check || options.compare;
|
|
@@ -479,9 +517,33 @@ async function runInventory(files, options) {
|
|
|
479
517
|
if (online) {
|
|
480
518
|
unresolved.forEach((a) => logger_1.logger.warn(`Not locked: could not resolve ${a.uses} (repository deleted, renamed or private, or the network is unavailable?)`));
|
|
481
519
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
520
|
+
// Given some of the workflows, lock what they use and keep the other
|
|
521
|
+
// workflows' entries as locked, instead of dropping them.
|
|
522
|
+
let entries = current;
|
|
523
|
+
const others = source ? otherWorkflowFiles(targets) : [];
|
|
524
|
+
if (source && others.length > 0) {
|
|
525
|
+
let previous;
|
|
526
|
+
try {
|
|
527
|
+
previous = (0, github_lockfile_1.readLockfile)(source);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
logger_1.logger.error(`Could not read ${lockName}: ${error instanceof Error ? error.message : 'Unknown error'}. Run without file arguments to rewrite it from every workflow.`);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
// actions.lock records no `via`, so its transitive entries can't be
|
|
534
|
+
// told apart from unused ones: keep them all.
|
|
535
|
+
const otherUses = github
|
|
536
|
+
? undefined
|
|
537
|
+
: await workflowUses(others, lockName);
|
|
538
|
+
const merged = (0, inventory_1.mergeLock)(previous.actions, current, otherUses);
|
|
539
|
+
entries = merged.actions;
|
|
540
|
+
if (merged.kept > 0) {
|
|
541
|
+
logger_1.logger.info(`Kept ${merged.kept} action(s) from ${lockName} for the other workflows.`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
fs.writeFileSync(lockPath, (0, github_lockfile_1.serializeSecureciLockfile)(entries));
|
|
545
|
+
const transitive = Object.values(entries).filter((e) => e.via).length;
|
|
546
|
+
logger_1.logger.success(`Wrote ${Object.keys(entries).length} action(s) to ${LOCKFILE_NAME}` +
|
|
485
547
|
(transitive > 0 ? ` (${transitive} used by other actions)` : '') +
|
|
486
548
|
'. Commit it to track supply-chain drift.');
|
|
487
549
|
return;
|
|
@@ -940,6 +1002,43 @@ configCmd
|
|
|
940
1002
|
(0, config_1.saveConfig)(config);
|
|
941
1003
|
logger_1.logger.success(`Unset ${key} in ${(0, config_1.getConfigFilePath)()}`);
|
|
942
1004
|
});
|
|
1005
|
+
const orgCmd = program
|
|
1006
|
+
.command('org')
|
|
1007
|
+
.description('Organisation-wide commands (for a SecureCI supply-chain assessment)');
|
|
1008
|
+
orgCmd
|
|
1009
|
+
.command('scan')
|
|
1010
|
+
.description('Scan the GitHub Actions workflows of every repository in an organisation and pack the JSON results for a SecureCI supply-chain assessment (runs org-scan.sh; source code is never downloaded)')
|
|
1011
|
+
.argument('[org]', 'GitHub organisation (or user) to scan')
|
|
1012
|
+
.option('--repos <file>', 'Scan only the repositories in this file (one "name" or "owner/name" per line)')
|
|
1013
|
+
.option('--out <dir>', 'Output directory (default: ./secureci-org-scan-<org>)')
|
|
1014
|
+
.option('-c, --contract <file>', 'Also check workflows against this pipeline contract (local file or owner/repo/path@ref)')
|
|
1015
|
+
.option('--include-forks', 'Also scan forked repositories')
|
|
1016
|
+
.option('--print-script', 'Print the script this command runs, to review it, and exit')
|
|
1017
|
+
.action(async (org, options) => {
|
|
1018
|
+
if (options.printScript) {
|
|
1019
|
+
process.stdout.write(org_scan_script_1.ORG_SCAN_SCRIPT);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (!org) {
|
|
1023
|
+
logger_1.logger.error('Missing <org>: secureci org scan <org>');
|
|
1024
|
+
process.exit(2);
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
const code = await (0, org_scan_1.runOrgScan)(org, options, {
|
|
1028
|
+
spawn: (command, args, spawnOptions) => (0, child_process_1.spawn)(command, args, spawnOptions),
|
|
1029
|
+
tmpdir: os.tmpdir,
|
|
1030
|
+
sea: (0, node_binary_1.isSea)(),
|
|
1031
|
+
execPath: process.execPath,
|
|
1032
|
+
entry: process.argv[1],
|
|
1033
|
+
env: process.env
|
|
1034
|
+
});
|
|
1035
|
+
process.exit(code);
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
logger_1.logger.error(error instanceof Error ? error.message : String(error));
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
943
1042
|
const cacheCmd = program
|
|
944
1043
|
.command('cache')
|
|
945
1044
|
.description('Manage the cache of downloaded remote actions');
|
package/dist/config.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.getConfigValue = getConfigValue;
|
|
|
44
44
|
exports.setConfigValue = setConfigValue;
|
|
45
45
|
exports.unsetConfigValue = unsetConfigValue;
|
|
46
46
|
exports.parseConfigValue = parseConfigValue;
|
|
47
|
+
exports.resolveIsolateWorkspace = resolveIsolateWorkspace;
|
|
47
48
|
exports.resolveNetworkDisabled = resolveNetworkDisabled;
|
|
48
49
|
const fs = __importStar(require("fs"));
|
|
49
50
|
const path = __importStar(require("path"));
|
|
@@ -114,6 +115,7 @@ exports.CONFIG_KEYS = [
|
|
|
114
115
|
'container-memory',
|
|
115
116
|
'allow-network',
|
|
116
117
|
'parallel',
|
|
118
|
+
'isolate-workspace',
|
|
117
119
|
'env-file',
|
|
118
120
|
'secret-file',
|
|
119
121
|
'input-file',
|
|
@@ -177,8 +179,9 @@ function parseConfigValue(key, rawValue) {
|
|
|
177
179
|
return num;
|
|
178
180
|
}
|
|
179
181
|
case 'allow-network':
|
|
182
|
+
case 'isolate-workspace':
|
|
180
183
|
if (rawValue !== 'true' && rawValue !== 'false') {
|
|
181
|
-
throw new Error(`Invalid value "${rawValue}" for
|
|
184
|
+
throw new Error(`Invalid value "${rawValue}" for ${key}. Must be "true" or "false".`);
|
|
182
185
|
}
|
|
183
186
|
return rawValue === 'true';
|
|
184
187
|
case 'parallel':
|
|
@@ -196,6 +199,14 @@ function parseConfigValue(key, rawValue) {
|
|
|
196
199
|
return rawValue;
|
|
197
200
|
}
|
|
198
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether each job runs in its own copy of the working copy:
|
|
204
|
+
* `--isolate-workspace` / `--no-isolate-workspace`, else
|
|
205
|
+
* `isolate-workspace` in the config file, else no.
|
|
206
|
+
*/
|
|
207
|
+
function resolveIsolateWorkspace(flag, config = loadConfig()) {
|
|
208
|
+
return flag ?? config['isolate-workspace'] === true;
|
|
209
|
+
}
|
|
199
210
|
/**
|
|
200
211
|
* Whether container networking should be disabled. Networking is on by
|
|
201
212
|
* default (matching GitHub-hosted runners); `--no-network` turns it off and
|
package/dist/inventory.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.collectReferences = collectReferences;
|
|
|
37
37
|
exports.buildInventory = buildInventory;
|
|
38
38
|
exports.addTransitive = addTransitive;
|
|
39
39
|
exports.diffLock = diffLock;
|
|
40
|
+
exports.mergeLock = mergeLock;
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
43
|
const workflow_parser_1 = require("./parser/workflow-parser");
|
|
@@ -293,4 +294,55 @@ function diffLock(current, lock) {
|
|
|
293
294
|
identity
|
|
294
295
|
};
|
|
295
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Merge entries resolved for some workflow files into the previous lock, so
|
|
299
|
+
* locking those files doesn't drop the other workflows' entries.
|
|
300
|
+
*
|
|
301
|
+
* `updated` wins. From `previous`, keep what the other workflows still use
|
|
302
|
+
* (`otherUses`, their direct references) and, following `via`, what those
|
|
303
|
+
* pull in; drop the rest. A reference used directly by another workflow has
|
|
304
|
+
* no `via`, and a dependency both sides pull in lists every reference that
|
|
305
|
+
* pulls it in. When `otherUses` is unknown (GitHub's actions.lock records no
|
|
306
|
+
* `via`, or a workflow couldn't be read), keep every previous entry.
|
|
307
|
+
*/
|
|
308
|
+
function mergeLock(previous, updated, otherUses) {
|
|
309
|
+
if (!otherUses) {
|
|
310
|
+
const kept = Object.keys(previous).filter((uses) => !(uses in updated));
|
|
311
|
+
return { actions: { ...previous, ...updated }, kept: kept.length };
|
|
312
|
+
}
|
|
313
|
+
// Previous entries the other workflows reach, taken as locked.
|
|
314
|
+
const kept = new Set([...otherUses].filter((uses) => uses in previous && !(uses in updated)));
|
|
315
|
+
for (let grew = true; grew;) {
|
|
316
|
+
grew = false;
|
|
317
|
+
for (const [uses, entry] of Object.entries(previous)) {
|
|
318
|
+
if (kept.has(uses) || uses in updated)
|
|
319
|
+
continue;
|
|
320
|
+
if (entry.via?.some((v) => kept.has(v))) {
|
|
321
|
+
kept.add(uses);
|
|
322
|
+
grew = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const withVia = (entry, via) => {
|
|
327
|
+
const out = { ...entry };
|
|
328
|
+
delete out.via;
|
|
329
|
+
if (via.length > 0)
|
|
330
|
+
out.via = [...new Set(via)].sort();
|
|
331
|
+
return out;
|
|
332
|
+
};
|
|
333
|
+
const stillLocked = (via) => (via ?? []).filter((v) => kept.has(v));
|
|
334
|
+
const actions = {};
|
|
335
|
+
for (const [uses, entry] of Object.entries(updated)) {
|
|
336
|
+
actions[uses] = otherUses.has(uses)
|
|
337
|
+
? withVia(entry, [])
|
|
338
|
+
: entry.via
|
|
339
|
+
? withVia(entry, [...entry.via, ...stillLocked(previous[uses]?.via)])
|
|
340
|
+
: entry;
|
|
341
|
+
}
|
|
342
|
+
for (const uses of kept) {
|
|
343
|
+
const entry = previous[uses];
|
|
344
|
+
actions[uses] = entry.via ? withVia(entry, stillLocked(entry.via)) : entry;
|
|
345
|
+
}
|
|
346
|
+
return { actions, kept: kept.size };
|
|
347
|
+
}
|
|
296
348
|
//# sourceMappingURL=inventory.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ORG_SCAN_SCRIPT = void 0;
|
|
4
|
+
// Generated from org-scan.sh by scripts/gen-org-scan-script.mjs; don't edit.
|
|
5
|
+
exports.ORG_SCAN_SCRIPT = "#!/usr/bin/env bash\n# Scan every GitHub Actions workflow in an organisation with the SecureCI CLI,\n# for a SecureCI supply-chain assessment.\n#\n# bash org-scan.sh <org>\n#\n# Read it before you run it (it's short, on purpose):\n#\n# curl -fsSL https://secure-ci.dev/org-scan.sh -o org-scan.sh\n# less org-scan.sh\n# GITHUB_TOKEN=... bash org-scan.sh my-org\n#\n# or, with the CLI installed: `secureci org scan my-org` runs this same script\n# (`secureci org scan --print-script` prints it).\n#\n# What it does, for each repository in the organisation (or in --repos):\n#\n# 1. Clones only the .github/ directory (a shallow, sparse clone: your\n# application code is never downloaded) into a temporary directory.\n# 2. Runs `secureci validate --format json` on its workflows, and\n# `secureci inventory --dry-run` on each workflow.\n# 3. Saves that JSON under the output directory and deletes the clone.\n#\n# Then it writes manifest.json (what was scanned, when, with which versions)\n# and packs everything into <output>.tar.gz. That archive is what you email to\n# hello@secure-ci.dev (if your policy doesn't allow email, tell us and we'll\n# agree another way).\n# It holds only the JSON results (repository and workflow names, the actions\n# they use, and the findings), never your source code. Look inside before you\n# send it: `tar -tzf <output>.tar.gz`.\n#\n# Archived repositories are skipped, and so are forks unless --include-forks.\n#\n# Your token stays on this machine: it is passed to curl on stdin and to git\n# through environment variables, never on a command line, and it is not\n# written to any output file.\n#\n# Requirements: bash, git 2.31+, curl, base64, tar, and the SecureCI CLI\n# (https://secure-ci.dev/install.sh or `npm install -g @secureci/cli`).\n#\n# Environment variables:\n# GITHUB_TOKEN or GH_TOKEN token that can read the organisation's\n# repositories (contents: read, metadata: read).\n# Needed for private repositories, and it raises\n# API rate limits. The CLI also uses it to resolve\n# action tags and look up security advisories.\n# GITHUB_SERVER_URL default https://github.com; set it for GitHub\n# Enterprise Server, e.g. https://github.example.com\n# GITHUB_API_URL default https://api.github.com, or\n# $GITHUB_SERVER_URL/api/v3 for Enterprise Server\n# SECURECI the secureci executable (default: secureci on PATH)\n#\n# Exit status: 0 when the scan finished (some repositories may have failed:\n# see the summary and errors.log), 1 on an error, 2 on a usage error.\n\nset -euo pipefail\n\nSCAN_SCRIPT_VERSION=1\nINSTALL_URL=\"https://secure-ci.dev/install.sh\"\nNAME_RE='^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'\n\nusage() {\n cat <<'EOF'\nUsage: org-scan.sh [options] <org>\n\nScan every GitHub Actions workflow in <org> with the SecureCI CLI and pack the\nJSON results into <output>.tar.gz for a SecureCI supply-chain assessment.\n\nOptions:\n --repos <file> scan only these repositories: one per line, \"name\" or\n \"owner/name\"; blank lines and # comments are ignored\n --out <dir> output directory (default: ./secureci-org-scan-<org>)\n --contract <file> also check workflows against this pipeline contract\n (a local file, or owner/repo/path@ref)\n --include-forks also scan forked repositories\n -h, --help show this help\n\nEnvironment: GITHUB_TOKEN (or GH_TOKEN), GITHUB_SERVER_URL, GITHUB_API_URL,\nSECURECI. See the comments at the top of this script.\nEOF\n}\n\nsay() {\n printf 'secureci-org-scan: %s\\n' \"$*\" >&2\n}\n\ndie() {\n say \"error: $*\"\n exit 1\n}\n\nusage_error() {\n say \"$*\"\n usage >&2\n exit 2\n}\n\nhas() {\n command -v \"$1\" >/dev/null 2>&1\n}\n\n# JSON string literal for $1.\njson_str() {\n local s=$1\n s=${s//\\\\/\\\\\\\\}\n s=${s//\\\"/\\\\\\\"}\n s=${s//$'\\t'/\\\\t}\n s=${s//$'\\n'/\\\\n}\n printf '\"%s\"' \"$s\"\n}\n\n# Run the secureci CLI (SECURECI_ENTRY is set when SECURECI is node itself).\nsecureci() {\n if [ -n \"${SECURECI_ENTRY:-}\" ]; then\n \"$SECURECI\" \"$SECURECI_ENTRY\" \"$@\"\n else\n \"$SECURECI\" \"$@\"\n fi\n}\n\n# GET an API URL into a file; prints the HTTP status. The token goes to curl\n# on stdin (--config -), so it never shows up in the process list.\napi_get() {\n local url=$1 out=$2\n {\n printf 'url = \"%s\"\\n' \"$url\"\n printf 'header = \"Accept: application/vnd.github+json\"\\n'\n printf 'header = \"X-GitHub-Api-Version: 2022-11-28\"\\n'\n if [ -n \"$TOKEN\" ]; then\n printf 'header = \"Authorization: Bearer %s\"\\n' \"$TOKEN\"\n fi\n } | curl --silent --show-error --location --config - \\\n --output \"$out\" --write-out '%{http_code}' || echo 000\n}\n\n# Print owner/name for each repository in a page of the repos API, skipping\n# archived repositories and (unless --include-forks) forks. Splitting on\n# commas puts each top-level key on its own line; keys inside strings are\n# escaped (\\\"full_name\\\") and can't match the anchored patterns.\nparse_repos() {\n tr ',' '\\n' <\"$1\" | awk -v forks=\"$INCLUDE_FORKS\" '\n function flush() {\n if (name != \"\" && archived != \"true\" && (forks == \"1\" || fork != \"true\"))\n print name\n name = \"\"; fork = \"\"; archived = \"\"\n }\n /^[[:space:]{[]*\"full_name\"[[:space:]]*:/ {\n flush()\n line = $0\n sub(/^[^:]*:[[:space:]]*\"/, \"\", line)\n sub(/\".*$/, \"\", line)\n name = line\n next\n }\n /^[[:space:]]*\"fork\"[[:space:]]*:/ {\n fork = ($0 ~ /true/) ? \"true\" : \"false\"; next\n }\n /^[[:space:]]*\"archived\"[[:space:]]*:/ {\n archived = ($0 ~ /true/) ? \"true\" : \"false\"; next\n }\n END { flush() }\n '\n}\n\n# List the organisation's repositories over the API (or the user's, when\n# <org> is a user account), one owner/name per line.\nlist_repos() {\n local base=\"$API_URL/orgs/$ORG/repos?per_page=100\" page=1 status\n local body=\"$WORK/page.json\"\n while :; do\n status=$(api_get \"$base&page=$page\" \"$body\")\n if [ \"$status\" = 404 ] && [ \"$page\" = 1 ] && [ \"${base#*/users/}\" = \"$base\" ]; then\n base=\"$API_URL/users/$ORG/repos?per_page=100&type=owner\"\n continue\n fi\n [ \"$status\" = 200 ] || die \"listing repositories for $ORG failed: HTTP $status from ${base%%\\?*} (check GITHUB_TOKEN and GITHUB_API_URL)\"\n # An empty page ends the listing.\n if ! grep -q '\"full_name\"' \"$body\"; then\n break\n fi\n parse_repos \"$body\"\n page=$((page + 1))\n done\n}\n\n# Repositories from a --repos file, one owner/name per line.\nread_repo_file() {\n local line\n while IFS= read -r line || [ -n \"$line\" ]; do\n line=${line%%#*}\n line=$(printf '%s' \"$line\" | tr -d '[:space:]')\n [ -n \"$line\" ] || continue\n case \"$line\" in\n */*) printf '%s\\n' \"$line\" ;;\n *) printf '%s/%s\\n' \"$ORG\" \"$line\" ;;\n esac\n done <\"$1\"\n}\n\n# git with the token as an HTTP header scoped to the server, passed through\n# GIT_CONFIG_* environment variables rather than arguments or a URL.\ngit_auth() {\n if [ -n \"$TOKEN\" ]; then\n local basic\n basic=$(printf 'x-access-token:%s' \"$TOKEN\" | base64 | tr -d '\\n')\n GIT_CONFIG_COUNT=1 \\\n GIT_CONFIG_KEY_0=\"http.$SERVER_URL/.extraheader\" \\\n GIT_CONFIG_VALUE_0=\"AUTHORIZATION: basic $basic\" \\\n GIT_TERMINAL_PROMPT=0 git \"$@\"\n else\n GIT_TERMINAL_PROMPT=0 git \"$@\"\n fi\n}\n\n# Clone .github/ of one repository and scan it. Sets STATUS and WORKFLOWS.\nscan_repo() {\n local full=$1 dir=\"$WORK/clone\" dest wf files=()\n STATUS=clone-failed\n WORKFLOWS=0\n rm -rf \"$dir\"\n if ! git_auth clone --quiet --depth 1 --filter=blob:none --sparse --no-tags \\\n \"$SERVER_URL/$full.git\" \"$dir\" 2>>\"$OUT/errors.log\" ||\n ! git -C \"$dir\" sparse-checkout set .github 2>>\"$OUT/errors.log\"; then\n echo \"[$full] clone failed\" >>\"$OUT/errors.log\"\n return\n fi\n\n for wf in \"$dir\"/.github/workflows/*.yml \"$dir\"/.github/workflows/*.yaml; do\n [ -f \"$wf\" ] && files+=(\".github/workflows/${wf##*/}\")\n done\n WORKFLOWS=${#files[@]}\n if [ \"$WORKFLOWS\" = 0 ]; then\n STATUS=no-workflows\n rm -rf \"$dir\"\n return\n fi\n\n dest=\"$OUT/repos/${full%%/*}__${full#*/}\"\n mkdir -p \"$dest/inventory\"\n echo \"[$full] secureci output:\" >>\"$OUT/errors.log\"\n # validate exits 1 when it finds errors; the JSON is still the result.\n (cd \"$dir\" && secureci validate --format json ${CONTRACT_ARG:+--contract \"$CONTRACT_ARG\"} \"${files[@]}\") \\\n >\"$dest/validate.json\" 2>>\"$OUT/errors.log\" || true\n for wf in \"${files[@]}\"; do\n (cd \"$dir\" && secureci inventory --dry-run \"$wf\") \\\n >\"$dest/inventory/${wf##*/}.json\" 2>>\"$OUT/errors.log\" || true\n done\n STATUS=scanned\n rm -rf \"$dir\"\n}\n\nmain() {\n ORG=\"\"\n OUT=\"\"\n REPO_FILE=\"\"\n CONTRACT=\"\"\n INCLUDE_FORKS=0\n while [ $# -gt 0 ]; do\n case \"$1\" in\n -h | --help) usage; exit 0 ;;\n --repos) [ $# -ge 2 ] || usage_error \"--repos needs a file\"; REPO_FILE=$2; shift 2 ;;\n --out) [ $# -ge 2 ] || usage_error \"--out needs a directory\"; OUT=$2; shift 2 ;;\n --contract) [ $# -ge 2 ] || usage_error \"--contract needs a file\"; CONTRACT=$2; shift 2 ;;\n --include-forks) INCLUDE_FORKS=1; shift ;;\n -*) usage_error \"unknown option: $1\" ;;\n *)\n [ -z \"$ORG\" ] || usage_error \"unexpected argument: $1\"\n ORG=$1; shift ;;\n esac\n done\n [ -n \"$ORG\" ] || usage_error \"missing <org>\"\n [[ $ORG =~ ^[A-Za-z0-9._-]+$ ]] || usage_error \"not an organisation name: $ORG\"\n\n SECURECI=${SECURECI:-secureci}\n for tool in git curl base64 tar; do\n has \"$tool\" || die \"$tool is required\"\n done\n has \"$SECURECI\" || die \"the SecureCI CLI ($SECURECI) was not found. Install it: curl -fsSL $INSTALL_URL | sh (or npm install -g @secureci/cli)\"\n SECURECI_VERSION=$(secureci --version 2>/dev/null) || die \"could not run $SECURECI --version\"\n\n TOKEN=${GITHUB_TOKEN:-${GH_TOKEN:-}}\n SERVER_URL=${GITHUB_SERVER_URL:-https://github.com}\n SERVER_URL=${SERVER_URL%/}\n if [ -z \"${GITHUB_API_URL:-}\" ]; then\n if [ \"$SERVER_URL\" = https://github.com ]; then\n API_URL=https://api.github.com\n else\n API_URL=\"$SERVER_URL/api/v3\"\n fi\n else\n API_URL=${GITHUB_API_URL%/}\n fi\n\n # A local contract is resolved now, because secureci runs inside each clone.\n CONTRACT_ARG=$CONTRACT\n if [ -n \"$CONTRACT\" ] && [ -f \"$CONTRACT\" ]; then\n CONTRACT_ARG=\"$(cd \"$(dirname \"$CONTRACT\")\" && pwd -P)/$(basename \"$CONTRACT\")\"\n fi\n if [ -n \"$REPO_FILE\" ] && [ ! -f \"$REPO_FILE\" ]; then\n die \"--repos file not found: $REPO_FILE\"\n fi\n\n OUT=${OUT:-secureci-org-scan-$ORG}\n OUT=${OUT%/}\n [ ! -e \"$OUT\" ] || die \"$OUT already exists; remove it or pass --out <dir>\"\n [ ! -e \"$OUT.tar.gz\" ] || die \"$OUT.tar.gz already exists; remove it or pass --out <dir>\"\n WORK=$(mktemp -d \"${TMPDIR:-/tmp}/secureci-org-scan.XXXXXX\")\n trap 'rm -rf \"$WORK\"' EXIT\n\n local repos=() full\n if [ -n \"$REPO_FILE\" ]; then\n while IFS= read -r full; do repos+=(\"$full\"); done < <(read_repo_file \"$REPO_FILE\")\n else\n say \"listing repositories in $ORG ($API_URL)\"\n list_repos >\"$WORK/repos.txt\"\n while IFS= read -r full; do repos+=(\"$full\"); done <\"$WORK/repos.txt\"\n fi\n\n local valid=()\n for full in ${repos[@]+\"${repos[@]}\"}; do\n if [[ $full =~ $NAME_RE ]] && [[ $full != *..* ]]; then\n valid+=(\"$full\")\n else\n say \"skipping unexpected repository name: $full\"\n fi\n done\n [ ${#valid[@]} -gt 0 ] || die \"no repositories to scan\"\n\n mkdir -p \"$OUT/repos\"\n : >\"$OUT/errors.log\"\n OUT=$(cd \"$OUT\" && pwd -P)\n\n local total=${#valid[@]} i=0 scanned=0 empty=0 failed=0 entries=\"\"\n for full in \"${valid[@]}\"; do\n i=$((i + 1))\n scan_repo \"$full\"\n case \"$STATUS\" in\n scanned) scanned=$((scanned + 1)) ;;\n no-workflows) empty=$((empty + 1)) ;;\n *) failed=$((failed + 1)) ;;\n esac\n say \"[$i/$total] $full: $STATUS ($WORKFLOWS workflows)\"\n entries+=\"${entries:+,}\n {\\\"name\\\": $(json_str \"$full\"), \\\"status\\\": \\\"$STATUS\\\", \\\"workflows\\\": $WORKFLOWS}\"\n done\n\n local contract_json=null\n [ -z \"$CONTRACT\" ] || contract_json=$(json_str \"$CONTRACT\")\n cat >\"$OUT/manifest.json\" <<EOF\n{\n \"tool\": \"secureci-org-scan\",\n \"scriptVersion\": $SCAN_SCRIPT_VERSION,\n \"secureciVersion\": $(json_str \"$SECURECI_VERSION\"),\n \"org\": $(json_str \"$ORG\"),\n \"server\": $(json_str \"$SERVER_URL\"),\n \"scannedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\n \"contract\": $contract_json,\n \"repos\": [$entries\n ]\n}\nEOF\n\n tar -czf \"$OUT.tar.gz\" -C \"$(dirname \"$OUT\")\" \"$(basename \"$OUT\")\"\n\n cat <<EOF\n\nScanned $total repositories in $ORG: $scanned scanned, $empty without workflows, $failed failed.\n\nResults: $OUT/\nArchive: $OUT.tar.gz\n\nThe archive holds only JSON results (repository and workflow names, the\nactions they use and the findings) plus errors.log, never source code.\nCheck it before you send it: tar -tzf $OUT.tar.gz\n\nEmail it to hello@secure-ci.dev. If your policy doesn't allow sending it by\nemail, write to us there and we'll agree another way.\nEOF\n}\n\nmain \"$@\"\n";
|
|
6
|
+
//# sourceMappingURL=org-scan-script.js.map
|
package/dist/org-scan.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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.orgScanArgs = orgScanArgs;
|
|
37
|
+
exports.orgScanEnv = orgScanEnv;
|
|
38
|
+
exports.runOrgScan = runOrgScan;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const org_scan_script_1 = require("./org-scan-script");
|
|
42
|
+
/** The script's arguments for `org` and the command's options. */
|
|
43
|
+
function orgScanArgs(org, options) {
|
|
44
|
+
const args = [];
|
|
45
|
+
if (options.repos)
|
|
46
|
+
args.push('--repos', options.repos);
|
|
47
|
+
if (options.out)
|
|
48
|
+
args.push('--out', options.out);
|
|
49
|
+
if (options.contract)
|
|
50
|
+
args.push('--contract', options.contract);
|
|
51
|
+
if (options.includeForks)
|
|
52
|
+
args.push('--include-forks');
|
|
53
|
+
return [...args, org];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How the script should run this same CLI: the standalone binary directly,
|
|
57
|
+
* or node with the CLI's entry file when installed with npm.
|
|
58
|
+
*/
|
|
59
|
+
function orgScanEnv(self) {
|
|
60
|
+
if (self.sea || !self.entry)
|
|
61
|
+
return { SECURECI: self.execPath };
|
|
62
|
+
return { SECURECI: self.execPath, SECURECI_ENTRY: self.entry };
|
|
63
|
+
}
|
|
64
|
+
/** Run the scan; resolves with the script's exit status. */
|
|
65
|
+
async function runOrgScan(org, options, deps) {
|
|
66
|
+
const dir = fs.mkdtempSync(path.join(deps.tmpdir(), 'secureci-org-scan-'));
|
|
67
|
+
const script = path.join(dir, 'org-scan.sh');
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(script, org_scan_script_1.ORG_SCAN_SCRIPT, { mode: 0o700 });
|
|
70
|
+
const env = { ...deps.env, ...orgScanEnv(deps) };
|
|
71
|
+
return await new Promise((resolve, reject) => {
|
|
72
|
+
const child = deps.spawn('bash', [script, ...orgScanArgs(org, options)], {
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
env
|
|
75
|
+
});
|
|
76
|
+
child.on('error', (error) => {
|
|
77
|
+
reject(error.code === 'ENOENT'
|
|
78
|
+
? new Error('`secureci org scan` needs bash. On Windows, run it from Git Bash or WSL.')
|
|
79
|
+
: error);
|
|
80
|
+
});
|
|
81
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=org-scan.js.map
|