@telora/daemon 0.15.37 → 0.15.40
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/build-info.json +2 -2
- package/dist/assembly-resolvers.d.ts +1 -1
- package/dist/assembly-resolvers.d.ts.map +1 -1
- package/dist/feeds/ghsa.d.ts +88 -0
- package/dist/feeds/ghsa.d.ts.map +1 -0
- package/dist/feeds/ghsa.js +219 -0
- package/dist/feeds/ghsa.js.map +1 -0
- package/dist/feeds/local.d.ts +55 -0
- package/dist/feeds/local.d.ts.map +1 -0
- package/dist/feeds/local.js +196 -0
- package/dist/feeds/local.js.map +1 -0
- package/dist/feeds/osv.d.ts +89 -0
- package/dist/feeds/osv.d.ts.map +1 -0
- package/dist/feeds/osv.js +266 -0
- package/dist/feeds/osv.js.map +1 -0
- package/dist/focus-engine.d.ts.map +1 -1
- package/dist/focus-engine.js +40 -0
- package/dist/focus-engine.js.map +1 -1
- package/dist/focus-executor.d.ts +53 -0
- package/dist/focus-executor.d.ts.map +1 -1
- package/dist/focus-executor.js +41 -26
- package/dist/focus-executor.js.map +1 -1
- package/dist/scanners/deps.d.ts +101 -0
- package/dist/scanners/deps.d.ts.map +1 -0
- package/dist/scanners/deps.js +242 -0
- package/dist/scanners/deps.js.map +1 -0
- package/dist/scanners/signatures.d.ts +44 -0
- package/dist/scanners/signatures.d.ts.map +1 -0
- package/dist/scanners/signatures.js +140 -0
- package/dist/scanners/signatures.js.map +1 -0
- package/dist/scanners/workflow.d.ts +34 -0
- package/dist/scanners/workflow.d.ts.map +1 -0
- package/dist/scanners/workflow.js +239 -0
- package/dist/scanners/workflow.js.map +1 -0
- package/dist/security-auto-inject.d.ts +114 -0
- package/dist/security-auto-inject.d.ts.map +1 -0
- package/dist/security-auto-inject.js +148 -0
- package/dist/security-auto-inject.js.map +1 -0
- package/dist/security-rescan-resolution.d.ts +84 -0
- package/dist/security-rescan-resolution.d.ts.map +1 -0
- package/dist/security-rescan-resolution.js +114 -0
- package/dist/security-rescan-resolution.js.map +1 -0
- package/dist/security-scan-engine.d.ts +96 -0
- package/dist/security-scan-engine.d.ts.map +1 -0
- package/dist/security-scan-engine.js +189 -0
- package/dist/security-scan-engine.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm audit signatures scanner -- security scan engine plugin.
|
|
3
|
+
*
|
|
4
|
+
* Runs `npm audit signatures --json` against the product's repo and converts
|
|
5
|
+
* each unverified or invalid-signature entry into a FindingDraft. Signature
|
|
6
|
+
* verification has no npm-provided severity tier, so every finding is marked
|
|
7
|
+
* 'medium' -- a signature problem is always actionable, but not on its own a
|
|
8
|
+
* critical-severity issue without further context.
|
|
9
|
+
*
|
|
10
|
+
* Pattern reference: verification-engine.ts (dep-injection seam for spawn).
|
|
11
|
+
*
|
|
12
|
+
* @module scanners/signatures
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
/** Default implementation: spawn the real npm CLI in the repo. */
|
|
17
|
+
const defaultRunNpmAuditSignatures = (cwd) => new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn('npm', ['audit', 'signatures', '--json'], {
|
|
19
|
+
cwd,
|
|
20
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
21
|
+
});
|
|
22
|
+
let stdout = '';
|
|
23
|
+
let stderr = '';
|
|
24
|
+
child.stdout?.on('data', (chunk) => {
|
|
25
|
+
stdout += chunk.toString('utf8');
|
|
26
|
+
});
|
|
27
|
+
child.stderr?.on('data', (chunk) => {
|
|
28
|
+
stderr += chunk.toString('utf8');
|
|
29
|
+
});
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
try {
|
|
32
|
+
child.kill('SIGKILL');
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
36
|
+
child.on('error', (err) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
reject(err);
|
|
39
|
+
});
|
|
40
|
+
child.on('close', (code) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
resolve({ stdout, stderr, exitCode: code });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function stringField(record, key) {
|
|
49
|
+
const v = record[key];
|
|
50
|
+
return typeof v === 'string' ? v : undefined;
|
|
51
|
+
}
|
|
52
|
+
function numberField(record, key) {
|
|
53
|
+
const v = record[key];
|
|
54
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
55
|
+
}
|
|
56
|
+
function entriesFromArray(value, fallbackReason) {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const item of value) {
|
|
61
|
+
if (!isRecord(item))
|
|
62
|
+
continue;
|
|
63
|
+
const name = stringField(item, 'name');
|
|
64
|
+
const version = stringField(item, 'version');
|
|
65
|
+
if (!name || !version)
|
|
66
|
+
continue;
|
|
67
|
+
const reason = stringField(item, 'reason') ?? fallbackReason;
|
|
68
|
+
out.push({ name, version, raw: item, reason });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse the npm audit signatures JSON envelope. Tolerates shape variations
|
|
74
|
+
* across npm versions -- `invalid` and `missing` may be absent or empty.
|
|
75
|
+
* Throws if the payload is not valid JSON or not an object.
|
|
76
|
+
*/
|
|
77
|
+
export function parseSignaturesReport(stdout) {
|
|
78
|
+
const trimmed = stdout.trim();
|
|
79
|
+
if (trimmed.length === 0) {
|
|
80
|
+
throw new Error('npm audit signatures produced empty output');
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(trimmed);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new Error(`failed to parse npm audit signatures JSON: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
if (!isRecord(parsed)) {
|
|
90
|
+
throw new Error('npm audit signatures JSON root is not an object');
|
|
91
|
+
}
|
|
92
|
+
const invalid = entriesFromArray(parsed.invalid, 'invalid_signature');
|
|
93
|
+
const missing = entriesFromArray(parsed.missing, 'missing');
|
|
94
|
+
// Best-effort total. npm has shipped a few different field names over the
|
|
95
|
+
// years (`packagesAudited`, `count`, etc.). Fall back to invalid+missing
|
|
96
|
+
// when nothing usable is reported.
|
|
97
|
+
const totalChecked = numberField(parsed, 'packagesAudited') ??
|
|
98
|
+
numberField(parsed, 'totalChecked') ??
|
|
99
|
+
(isRecord(parsed.count) ? numberField(parsed.count, 'total') : undefined) ??
|
|
100
|
+
invalid.length + missing.length;
|
|
101
|
+
return { invalid, missing, totalChecked };
|
|
102
|
+
}
|
|
103
|
+
export function createSignaturesScanner(deps) {
|
|
104
|
+
return {
|
|
105
|
+
iocClass: 'signatures',
|
|
106
|
+
async scan(ctx) {
|
|
107
|
+
const { stdout, exitCode } = await deps.runNpmAuditSignatures(ctx.repoPath);
|
|
108
|
+
const report = parseSignaturesReport(stdout);
|
|
109
|
+
const findings = [];
|
|
110
|
+
const emit = (entry) => {
|
|
111
|
+
findings.push({
|
|
112
|
+
iocClass: 'signatures',
|
|
113
|
+
severity: 'medium',
|
|
114
|
+
identifier: `${entry.name}@${entry.version}`,
|
|
115
|
+
payload: {
|
|
116
|
+
...entry.raw,
|
|
117
|
+
reason: entry.reason,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
for (const entry of report.invalid)
|
|
122
|
+
emit(entry);
|
|
123
|
+
for (const entry of report.missing)
|
|
124
|
+
emit(entry);
|
|
125
|
+
const coverage = {
|
|
126
|
+
packages_checked: report.totalChecked,
|
|
127
|
+
unverified: report.invalid.length + report.missing.length,
|
|
128
|
+
missing: report.missing.length,
|
|
129
|
+
invalid: report.invalid.length,
|
|
130
|
+
npm_exit_code: exitCode,
|
|
131
|
+
};
|
|
132
|
+
return { findings, coverage };
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Default scanner wired to the real npm CLI. */
|
|
137
|
+
export const signaturesScanner = createSignaturesScanner({
|
|
138
|
+
runNpmAuditSignatures: defaultRunNpmAuditSignatures,
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=signatures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signatures.js","sourceRoot":"","sources":["../../src/scanners/signatures.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions workflow scanner -- security-scan engine plugin.
|
|
3
|
+
*
|
|
4
|
+
* Detects the `pull_request_target` + checkout-head + cache-write pattern
|
|
5
|
+
* that lets a fork PR's untrusted code execute with the base repo's
|
|
6
|
+
* elevated permissions and secrets, and persists artifacts (caches)
|
|
7
|
+
* back to the base repo. This is the canonical "pwn request" supply
|
|
8
|
+
* chain vulnerability.
|
|
9
|
+
*
|
|
10
|
+
* Matched when ALL three are true for a single job:
|
|
11
|
+
* 1. Workflow `on:` includes `pull_request_target` (string | array | object).
|
|
12
|
+
* 2. The job contains an `actions/checkout@*` step whose `with.ref` is
|
|
13
|
+
* one of `${{ github.event.pull_request.head.sha }}` or
|
|
14
|
+
* `${{ github.event.pull_request.head.ref }}`.
|
|
15
|
+
* 3. The job contains a cache-write step:
|
|
16
|
+
* - `actions/cache@*` (any non-`restore`-suffixed action), or
|
|
17
|
+
* - `actions/setup-*` step with `with.cache: true` (or any truthy
|
|
18
|
+
* string), or
|
|
19
|
+
* - any step after the head-ref checkout whose `with.cache` is truthy.
|
|
20
|
+
*
|
|
21
|
+
* Pattern reference: verification-engine.ts (Deps + default factory).
|
|
22
|
+
*
|
|
23
|
+
* @module scanners/workflow
|
|
24
|
+
*/
|
|
25
|
+
import type { Scanner } from '../security-scan-engine.js';
|
|
26
|
+
export interface WorkflowScannerDeps {
|
|
27
|
+
/** List workflow files in the repo's `.github/workflows/` directory. */
|
|
28
|
+
readWorkflowsDir: (repoPath: string) => Promise<string[]>;
|
|
29
|
+
/** Read a workflow file's contents as UTF-8. */
|
|
30
|
+
readFile: (path: string) => Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
export declare function createWorkflowScanner(deps?: WorkflowScannerDeps): Scanner;
|
|
33
|
+
export declare const workflowScanner: Scanner;
|
|
34
|
+
//# sourceMappingURL=workflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/scanners/workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAyC,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAMjG,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1D,gDAAgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAmKD,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,mBAAiC,GAAG,OAAO,CA4DtF;AAWD,eAAO,MAAM,eAAe,EAAE,OAAiC,CAAC"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions workflow scanner -- security-scan engine plugin.
|
|
3
|
+
*
|
|
4
|
+
* Detects the `pull_request_target` + checkout-head + cache-write pattern
|
|
5
|
+
* that lets a fork PR's untrusted code execute with the base repo's
|
|
6
|
+
* elevated permissions and secrets, and persists artifacts (caches)
|
|
7
|
+
* back to the base repo. This is the canonical "pwn request" supply
|
|
8
|
+
* chain vulnerability.
|
|
9
|
+
*
|
|
10
|
+
* Matched when ALL three are true for a single job:
|
|
11
|
+
* 1. Workflow `on:` includes `pull_request_target` (string | array | object).
|
|
12
|
+
* 2. The job contains an `actions/checkout@*` step whose `with.ref` is
|
|
13
|
+
* one of `${{ github.event.pull_request.head.sha }}` or
|
|
14
|
+
* `${{ github.event.pull_request.head.ref }}`.
|
|
15
|
+
* 3. The job contains a cache-write step:
|
|
16
|
+
* - `actions/cache@*` (any non-`restore`-suffixed action), or
|
|
17
|
+
* - `actions/setup-*` step with `with.cache: true` (or any truthy
|
|
18
|
+
* string), or
|
|
19
|
+
* - any step after the head-ref checkout whose `with.cache` is truthy.
|
|
20
|
+
*
|
|
21
|
+
* Pattern reference: verification-engine.ts (Deps + default factory).
|
|
22
|
+
*
|
|
23
|
+
* @module scanners/workflow
|
|
24
|
+
*/
|
|
25
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { parse as parseYaml } from 'yaml';
|
|
28
|
+
const WORKFLOW_EXTENSIONS = ['.yml', '.yaml'];
|
|
29
|
+
const CHECKOUT_REF_HEAD_SHA = '${{ github.event.pull_request.head.sha }}';
|
|
30
|
+
const CHECKOUT_REF_HEAD_REF = '${{ github.event.pull_request.head.ref }}';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Defaults -- real fs
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const defaultDeps = {
|
|
35
|
+
readWorkflowsDir: async (repoPath) => {
|
|
36
|
+
const dir = join(repoPath, '.github', 'workflows');
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await readdir(dir);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return entries
|
|
45
|
+
.filter((name) => WORKFLOW_EXTENSIONS.some((ext) => name.endsWith(ext)))
|
|
46
|
+
.map((name) => join(dir, name));
|
|
47
|
+
},
|
|
48
|
+
readFile: (path) => readFile(path, 'utf8'),
|
|
49
|
+
};
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Type guards -- narrow `unknown` parsed YAML
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
function isStringArray(value) {
|
|
57
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
58
|
+
}
|
|
59
|
+
/** Returns true when the workflow `on:` declaration includes pull_request_target. */
|
|
60
|
+
function hasPullRequestTargetTrigger(on) {
|
|
61
|
+
if (typeof on === 'string')
|
|
62
|
+
return on === 'pull_request_target';
|
|
63
|
+
if (isStringArray(on))
|
|
64
|
+
return on.includes('pull_request_target');
|
|
65
|
+
if (isRecord(on))
|
|
66
|
+
return Object.prototype.hasOwnProperty.call(on, 'pull_request_target');
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function asStep(value) {
|
|
70
|
+
if (!isRecord(value))
|
|
71
|
+
return null;
|
|
72
|
+
const step = {};
|
|
73
|
+
if (typeof value.uses === 'string')
|
|
74
|
+
step.uses = value.uses;
|
|
75
|
+
if (typeof value.run === 'string')
|
|
76
|
+
step.run = value.run;
|
|
77
|
+
if (typeof value.name === 'string')
|
|
78
|
+
step.name = value.name;
|
|
79
|
+
if (isRecord(value.with))
|
|
80
|
+
step.with = value.with;
|
|
81
|
+
return step;
|
|
82
|
+
}
|
|
83
|
+
function getActionName(uses) {
|
|
84
|
+
// Strip @ref and any subpath after a slash beyond owner/repo. We only need
|
|
85
|
+
// the canonical `owner/repo` (or `owner/repo/path`) for prefix matching.
|
|
86
|
+
const atIdx = uses.indexOf('@');
|
|
87
|
+
return atIdx >= 0 ? uses.slice(0, atIdx) : uses;
|
|
88
|
+
}
|
|
89
|
+
function isCheckoutAction(step) {
|
|
90
|
+
if (!step.uses)
|
|
91
|
+
return false;
|
|
92
|
+
return getActionName(step.uses) === 'actions/checkout';
|
|
93
|
+
}
|
|
94
|
+
function getCheckoutRef(step) {
|
|
95
|
+
const ref = step.with?.ref;
|
|
96
|
+
return typeof ref === 'string' ? ref : null;
|
|
97
|
+
}
|
|
98
|
+
function checkoutTargetsForkHead(step) {
|
|
99
|
+
const ref = getCheckoutRef(step);
|
|
100
|
+
if (ref === null)
|
|
101
|
+
return false;
|
|
102
|
+
return ref === CHECKOUT_REF_HEAD_SHA || ref === CHECKOUT_REF_HEAD_REF;
|
|
103
|
+
}
|
|
104
|
+
/** True when `with.cache` is set to a truthy value (boolean true or any non-empty/non-false string). */
|
|
105
|
+
function hasTruthyCacheInput(step) {
|
|
106
|
+
const cache = step.with?.cache;
|
|
107
|
+
if (cache === true)
|
|
108
|
+
return true;
|
|
109
|
+
if (typeof cache === 'string') {
|
|
110
|
+
const normalized = cache.trim().toLowerCase();
|
|
111
|
+
return normalized.length > 0 && normalized !== 'false' && normalized !== '0';
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
function isCacheWriteStep(step) {
|
|
116
|
+
if (step.uses) {
|
|
117
|
+
const action = getActionName(step.uses);
|
|
118
|
+
// `actions/cache@v4` writes by default. `actions/cache/restore` only restores.
|
|
119
|
+
if (action === 'actions/cache')
|
|
120
|
+
return true;
|
|
121
|
+
// `actions/setup-node`, `actions/setup-python`, etc. with cache: true.
|
|
122
|
+
if (action.startsWith('actions/setup-') && hasTruthyCacheInput(step))
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
// Any step that opts into a cache via with.cache (e.g. third-party setup actions).
|
|
126
|
+
if (hasTruthyCacheInput(step))
|
|
127
|
+
return true;
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
function analyzeJob(job) {
|
|
131
|
+
const result = {
|
|
132
|
+
hasCheckoutOfForkHead: false,
|
|
133
|
+
cacheWriteAfterCheckout: false,
|
|
134
|
+
matchedLines: [],
|
|
135
|
+
};
|
|
136
|
+
if (!isRecord(job))
|
|
137
|
+
return result;
|
|
138
|
+
const steps = job.steps;
|
|
139
|
+
if (!Array.isArray(steps))
|
|
140
|
+
return result;
|
|
141
|
+
let sawForkHeadCheckout = false;
|
|
142
|
+
for (const raw of steps) {
|
|
143
|
+
const step = asStep(raw);
|
|
144
|
+
if (!step)
|
|
145
|
+
continue;
|
|
146
|
+
if (isCheckoutAction(step) && checkoutTargetsForkHead(step)) {
|
|
147
|
+
sawForkHeadCheckout = true;
|
|
148
|
+
result.hasCheckoutOfForkHead = true;
|
|
149
|
+
const ref = getCheckoutRef(step) ?? '';
|
|
150
|
+
result.matchedLines.push(`uses: ${step.uses} with ref: ${ref}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (sawForkHeadCheckout && isCacheWriteStep(step)) {
|
|
154
|
+
result.cacheWriteAfterCheckout = true;
|
|
155
|
+
if (step.uses) {
|
|
156
|
+
const cache = step.with?.cache;
|
|
157
|
+
const cacheNote = cache !== undefined ? ` (cache: ${String(cache)})` : '';
|
|
158
|
+
result.matchedLines.push(`uses: ${step.uses}${cacheNote}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
result.matchedLines.push(`step: ${step.name ?? '(unnamed)'} with cache`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Factory
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
const PATTERN_LABEL = 'pull_request_target + checkout-head + cache-write';
|
|
171
|
+
export function createWorkflowScanner(deps = defaultDeps) {
|
|
172
|
+
return {
|
|
173
|
+
iocClass: 'workflow',
|
|
174
|
+
async scan(ctx) {
|
|
175
|
+
const files = await deps.readWorkflowsDir(ctx.repoPath);
|
|
176
|
+
const findings = [];
|
|
177
|
+
let parseErrors = 0;
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
let raw;
|
|
180
|
+
try {
|
|
181
|
+
raw = await deps.readFile(file);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
parseErrors += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = parseYaml(raw);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
parseErrors += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!isRecord(parsed))
|
|
196
|
+
continue;
|
|
197
|
+
if (!hasPullRequestTargetTrigger(parsed.on))
|
|
198
|
+
continue;
|
|
199
|
+
const jobs = parsed.jobs;
|
|
200
|
+
if (!isRecord(jobs))
|
|
201
|
+
continue;
|
|
202
|
+
const workflowFileName = baseName(file);
|
|
203
|
+
for (const [jobName, jobValue] of Object.entries(jobs)) {
|
|
204
|
+
const analysis = analyzeJob(jobValue);
|
|
205
|
+
if (!analysis.hasCheckoutOfForkHead || !analysis.cacheWriteAfterCheckout)
|
|
206
|
+
continue;
|
|
207
|
+
findings.push({
|
|
208
|
+
iocClass: 'workflow',
|
|
209
|
+
severity: 'high',
|
|
210
|
+
identifier: `${workflowFileName}:${jobName}`,
|
|
211
|
+
payload: {
|
|
212
|
+
workflow: workflowFileName,
|
|
213
|
+
job: jobName,
|
|
214
|
+
pattern: PATTERN_LABEL,
|
|
215
|
+
matched_lines: analysis.matchedLines,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
findings,
|
|
222
|
+
coverage: {
|
|
223
|
+
workflows_scanned: files.length,
|
|
224
|
+
risky_patterns: findings.length,
|
|
225
|
+
parse_errors: parseErrors,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function baseName(p) {
|
|
232
|
+
const slash = p.lastIndexOf('/');
|
|
233
|
+
return slash >= 0 ? p.slice(slash + 1) : p;
|
|
234
|
+
}
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Default scanner -- real fs IO
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
export const workflowScanner = createWorkflowScanner();
|
|
239
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.js","sourceRoot":"","sources":["../../src/scanners/workflow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Severity-gated auto-injection for security findings.
|
|
3
|
+
*
|
|
4
|
+
* When the scanner engine writes a finding at or above the configured
|
|
5
|
+
* auto_inject_severity_threshold, this module:
|
|
6
|
+
* 1. Resolves the product's Security focus (focus_kind='security').
|
|
7
|
+
* 2. Creates or reuses an entity node representing the vulnerable surface.
|
|
8
|
+
* 3. Creates an injection node on the focus's reality tree with
|
|
9
|
+
* statement = advisory summary; targets the entity node via a
|
|
10
|
+
* reality_tree_edge of kind 'targets'.
|
|
11
|
+
* 4. Creates a delivery on the Security focus linked to the injection
|
|
12
|
+
* via the delivery's injection_id column.
|
|
13
|
+
* 5. Sets finding.linked_injection_id.
|
|
14
|
+
* 6. Writes a security_finding_audit row with action='auto_injected'.
|
|
15
|
+
*
|
|
16
|
+
* Below-threshold findings are not auto-injected; the FindingsView UI
|
|
17
|
+
* offers click-to-remediate that calls this same function with a
|
|
18
|
+
* `force: true` flag (single code path for both auto and manual).
|
|
19
|
+
*
|
|
20
|
+
* Suppressed findings (suppression jsonb non-null and not expired) are
|
|
21
|
+
* skipped entirely.
|
|
22
|
+
*
|
|
23
|
+
* @module security-auto-inject
|
|
24
|
+
*/
|
|
25
|
+
import type { Severity } from './security-scan-engine.js';
|
|
26
|
+
export interface FindingForInjection {
|
|
27
|
+
id: string;
|
|
28
|
+
organizationId: string;
|
|
29
|
+
productId: string;
|
|
30
|
+
iocClass: string;
|
|
31
|
+
severity: Severity;
|
|
32
|
+
identifier: string;
|
|
33
|
+
payload: Record<string, unknown>;
|
|
34
|
+
status: 'open' | 'resolved' | 'suppressed';
|
|
35
|
+
suppression: Record<string, unknown> | null;
|
|
36
|
+
linkedInjectionId: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface AutoInjectOptions {
|
|
39
|
+
/** Threshold from the scan config (default 'critical'). */
|
|
40
|
+
autoInjectThreshold: Severity;
|
|
41
|
+
/**
|
|
42
|
+
* When true, bypass the severity threshold (used by the UI's
|
|
43
|
+
* 'Click to remediate' button on below-threshold findings). The
|
|
44
|
+
* suppression and already-linked checks still apply.
|
|
45
|
+
*/
|
|
46
|
+
force?: boolean;
|
|
47
|
+
/** Actor user id, if the action came from a UI button rather than the daemon. */
|
|
48
|
+
actorUserId?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface AutoInjectDeps {
|
|
51
|
+
/** Look up the Security focus id + tree id for a product. */
|
|
52
|
+
resolveSecurityFocus: (productId: string) => Promise<{
|
|
53
|
+
focusId: string;
|
|
54
|
+
treeId: string;
|
|
55
|
+
} | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Create or find an entity node representing the vulnerable surface
|
|
58
|
+
* (one per identifier per focus, deduplicated server-side).
|
|
59
|
+
*/
|
|
60
|
+
upsertEntityNode: (input: {
|
|
61
|
+
treeId: string;
|
|
62
|
+
organizationId: string;
|
|
63
|
+
label: string;
|
|
64
|
+
payload: Record<string, unknown>;
|
|
65
|
+
}) => Promise<{
|
|
66
|
+
nodeId: string;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Create an injection node + targets edge in a single compound op.
|
|
70
|
+
*/
|
|
71
|
+
createInjection: (input: {
|
|
72
|
+
treeId: string;
|
|
73
|
+
organizationId: string;
|
|
74
|
+
statement: string;
|
|
75
|
+
targetNodeId: string;
|
|
76
|
+
sourcePayload: Record<string, unknown>;
|
|
77
|
+
}) => Promise<{
|
|
78
|
+
nodeId: string;
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* Materialize a delivery on the Security focus, linked to the injection.
|
|
82
|
+
*/
|
|
83
|
+
createDelivery: (input: {
|
|
84
|
+
focusId: string;
|
|
85
|
+
productId: string;
|
|
86
|
+
organizationId: string;
|
|
87
|
+
name: string;
|
|
88
|
+
description: string;
|
|
89
|
+
injectionNodeId: string;
|
|
90
|
+
}) => Promise<{
|
|
91
|
+
deliveryId: string;
|
|
92
|
+
}>;
|
|
93
|
+
/** Update the finding row with its new linked_injection_id. */
|
|
94
|
+
linkFinding: (findingId: string, injectionNodeId: string) => Promise<void>;
|
|
95
|
+
/** Append a finding audit row. */
|
|
96
|
+
writeAudit: (input: {
|
|
97
|
+
findingId: string;
|
|
98
|
+
organizationId: string;
|
|
99
|
+
action: 'auto_injected' | 'manually_remediated';
|
|
100
|
+
actorUserId?: string;
|
|
101
|
+
reason?: string;
|
|
102
|
+
payload?: Record<string, unknown>;
|
|
103
|
+
}) => Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
/** Returns true when `severity` is at or above `threshold`. */
|
|
106
|
+
export declare function meetsThreshold(severity: Severity, threshold: Severity): boolean;
|
|
107
|
+
export interface ProcessNewFindingResult {
|
|
108
|
+
status: 'injected' | 'skipped_below_threshold' | 'skipped_already_linked' | 'skipped_suppressed' | 'skipped_no_security_focus';
|
|
109
|
+
injectionNodeId?: string;
|
|
110
|
+
deliveryId?: string;
|
|
111
|
+
}
|
|
112
|
+
export declare function processNewFinding(finding: FindingForInjection, options: AutoInjectOptions, deps: AutoInjectDeps): Promise<ProcessNewFindingResult>;
|
|
113
|
+
export declare function buildDefaultAutoInjectDeps(): AutoInjectDeps;
|
|
114
|
+
//# sourceMappingURL=security-auto-inject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-auto-inject.d.ts","sourceRoot":"","sources":["../src/security-auto-inject.ts"],"names":[],"mappings":"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"}
|