@telora/daemon 0.15.37 → 0.15.42

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.
Files changed (47) hide show
  1. package/build-info.json +2 -2
  2. package/dist/assembly-resolvers.d.ts +1 -1
  3. package/dist/assembly-resolvers.d.ts.map +1 -1
  4. package/dist/feeds/ghsa.d.ts +97 -0
  5. package/dist/feeds/ghsa.d.ts.map +1 -0
  6. package/dist/feeds/ghsa.js +226 -0
  7. package/dist/feeds/ghsa.js.map +1 -0
  8. package/dist/feeds/local.d.ts +55 -0
  9. package/dist/feeds/local.d.ts.map +1 -0
  10. package/dist/feeds/local.js +196 -0
  11. package/dist/feeds/local.js.map +1 -0
  12. package/dist/feeds/osv.d.ts +98 -0
  13. package/dist/feeds/osv.d.ts.map +1 -0
  14. package/dist/feeds/osv.js +277 -0
  15. package/dist/feeds/osv.js.map +1 -0
  16. package/dist/focus-engine.d.ts.map +1 -1
  17. package/dist/focus-engine.js +47 -0
  18. package/dist/focus-engine.js.map +1 -1
  19. package/dist/focus-executor.d.ts +53 -0
  20. package/dist/focus-executor.d.ts.map +1 -1
  21. package/dist/focus-executor.js +41 -26
  22. package/dist/focus-executor.js.map +1 -1
  23. package/dist/scanners/deps.d.ts +101 -0
  24. package/dist/scanners/deps.d.ts.map +1 -0
  25. package/dist/scanners/deps.js +242 -0
  26. package/dist/scanners/deps.js.map +1 -0
  27. package/dist/scanners/signatures.d.ts +44 -0
  28. package/dist/scanners/signatures.d.ts.map +1 -0
  29. package/dist/scanners/signatures.js +140 -0
  30. package/dist/scanners/signatures.js.map +1 -0
  31. package/dist/scanners/workflow.d.ts +42 -0
  32. package/dist/scanners/workflow.d.ts.map +1 -0
  33. package/dist/scanners/workflow.js +325 -0
  34. package/dist/scanners/workflow.js.map +1 -0
  35. package/dist/security-auto-inject.d.ts +114 -0
  36. package/dist/security-auto-inject.d.ts.map +1 -0
  37. package/dist/security-auto-inject.js +148 -0
  38. package/dist/security-auto-inject.js.map +1 -0
  39. package/dist/security-rescan-resolution.d.ts +84 -0
  40. package/dist/security-rescan-resolution.d.ts.map +1 -0
  41. package/dist/security-rescan-resolution.js +114 -0
  42. package/dist/security-rescan-resolution.js.map +1 -0
  43. package/dist/security-scan-engine.d.ts +102 -0
  44. package/dist/security-scan-engine.d.ts.map +1 -0
  45. package/dist/security-scan-engine.js +202 -0
  46. package/dist/security-scan-engine.js.map +1 -0
  47. 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,42 @@
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
+ import { type IocFeedEntry } from '../feeds/local.js';
27
+ export interface WorkflowScannerDeps {
28
+ /** List workflow files in the repo's `.github/workflows/` directory. */
29
+ readWorkflowsDir: (repoPath: string) => Promise<string[]>;
30
+ /** Read a workflow file's contents as UTF-8. */
31
+ readFile: (path: string) => Promise<string>;
32
+ /**
33
+ * Load org-local IOC feed entries with `ioc_class='workflow'`. Each
34
+ * entry's pattern is tested against the workflow file's full text and
35
+ * against every step's `uses`, `run`, and `name` value. Default loads
36
+ * from the daemon edge function; tests inject a stub.
37
+ */
38
+ loadLocalIocFeed?: (organizationId: string, iocClass?: string) => Promise<IocFeedEntry[]>;
39
+ }
40
+ export declare function createWorkflowScanner(deps?: WorkflowScannerDeps): Scanner;
41
+ export declare const workflowScanner: Scanner;
42
+ //# 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;AACjG,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAMxF,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;IAC5C;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAC3F;AAoMD,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,mBAAiC,GAAG,OAAO,CAkHtF;AAWD,eAAO,MAAM,eAAe,EAAE,OAAiC,CAAC"}
@@ -0,0 +1,325 @@
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
+ import { loadLocalIocFeed, matchesPattern } from '../feeds/local.js';
29
+ const WORKFLOW_EXTENSIONS = ['.yml', '.yaml'];
30
+ const CHECKOUT_REF_HEAD_SHA = '${{ github.event.pull_request.head.sha }}';
31
+ const CHECKOUT_REF_HEAD_REF = '${{ github.event.pull_request.head.ref }}';
32
+ // ---------------------------------------------------------------------------
33
+ // Defaults -- real fs
34
+ // ---------------------------------------------------------------------------
35
+ const defaultDeps = {
36
+ readWorkflowsDir: async (repoPath) => {
37
+ const dir = join(repoPath, '.github', 'workflows');
38
+ let entries;
39
+ try {
40
+ entries = await readdir(dir);
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ return entries
46
+ .filter((name) => WORKFLOW_EXTENSIONS.some((ext) => name.endsWith(ext)))
47
+ .map((name) => join(dir, name));
48
+ },
49
+ readFile: (path) => readFile(path, 'utf8'),
50
+ loadLocalIocFeed,
51
+ };
52
+ // ---------------------------------------------------------------------------
53
+ // Type guards -- narrow `unknown` parsed YAML
54
+ // ---------------------------------------------------------------------------
55
+ function isRecord(value) {
56
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
57
+ }
58
+ function isStringArray(value) {
59
+ return Array.isArray(value) && value.every((v) => typeof v === 'string');
60
+ }
61
+ /** Returns true when the workflow `on:` declaration includes pull_request_target. */
62
+ function hasPullRequestTargetTrigger(on) {
63
+ if (typeof on === 'string')
64
+ return on === 'pull_request_target';
65
+ if (isStringArray(on))
66
+ return on.includes('pull_request_target');
67
+ if (isRecord(on))
68
+ return Object.prototype.hasOwnProperty.call(on, 'pull_request_target');
69
+ return false;
70
+ }
71
+ function asStep(value) {
72
+ if (!isRecord(value))
73
+ return null;
74
+ const step = {};
75
+ if (typeof value.uses === 'string')
76
+ step.uses = value.uses;
77
+ if (typeof value.run === 'string')
78
+ step.run = value.run;
79
+ if (typeof value.name === 'string')
80
+ step.name = value.name;
81
+ if (isRecord(value.with))
82
+ step.with = value.with;
83
+ return step;
84
+ }
85
+ function getActionName(uses) {
86
+ // Strip @ref and any subpath after a slash beyond owner/repo. We only need
87
+ // the canonical `owner/repo` (or `owner/repo/path`) for prefix matching.
88
+ const atIdx = uses.indexOf('@');
89
+ return atIdx >= 0 ? uses.slice(0, atIdx) : uses;
90
+ }
91
+ function isCheckoutAction(step) {
92
+ if (!step.uses)
93
+ return false;
94
+ return getActionName(step.uses) === 'actions/checkout';
95
+ }
96
+ function getCheckoutRef(step) {
97
+ const ref = step.with?.ref;
98
+ return typeof ref === 'string' ? ref : null;
99
+ }
100
+ function checkoutTargetsForkHead(step) {
101
+ const ref = getCheckoutRef(step);
102
+ if (ref === null)
103
+ return false;
104
+ return ref === CHECKOUT_REF_HEAD_SHA || ref === CHECKOUT_REF_HEAD_REF;
105
+ }
106
+ /** True when `with.cache` is set to a truthy value (boolean true or any non-empty/non-false string). */
107
+ function hasTruthyCacheInput(step) {
108
+ const cache = step.with?.cache;
109
+ if (cache === true)
110
+ return true;
111
+ if (typeof cache === 'string') {
112
+ const normalized = cache.trim().toLowerCase();
113
+ return normalized.length > 0 && normalized !== 'false' && normalized !== '0';
114
+ }
115
+ return false;
116
+ }
117
+ function isCacheWriteStep(step) {
118
+ if (step.uses) {
119
+ const action = getActionName(step.uses);
120
+ // `actions/cache@v4` writes by default. `actions/cache/restore` only restores.
121
+ if (action === 'actions/cache')
122
+ return true;
123
+ // `actions/setup-node`, `actions/setup-python`, etc. with cache: true.
124
+ if (action.startsWith('actions/setup-') && hasTruthyCacheInput(step))
125
+ return true;
126
+ }
127
+ // Any step that opts into a cache via with.cache (e.g. third-party setup actions).
128
+ if (hasTruthyCacheInput(step))
129
+ return true;
130
+ return false;
131
+ }
132
+ function analyzeJob(job) {
133
+ const result = {
134
+ hasCheckoutOfForkHead: false,
135
+ cacheWriteAfterCheckout: false,
136
+ matchedLines: [],
137
+ };
138
+ if (!isRecord(job))
139
+ return result;
140
+ const steps = job.steps;
141
+ if (!Array.isArray(steps))
142
+ return result;
143
+ let sawForkHeadCheckout = false;
144
+ for (const raw of steps) {
145
+ const step = asStep(raw);
146
+ if (!step)
147
+ continue;
148
+ if (isCheckoutAction(step) && checkoutTargetsForkHead(step)) {
149
+ sawForkHeadCheckout = true;
150
+ result.hasCheckoutOfForkHead = true;
151
+ const ref = getCheckoutRef(step) ?? '';
152
+ result.matchedLines.push(`uses: ${step.uses} with ref: ${ref}`);
153
+ continue;
154
+ }
155
+ if (sawForkHeadCheckout && isCacheWriteStep(step)) {
156
+ result.cacheWriteAfterCheckout = true;
157
+ if (step.uses) {
158
+ const cache = step.with?.cache;
159
+ const cacheNote = cache !== undefined ? ` (cache: ${String(cache)})` : '';
160
+ result.matchedLines.push(`uses: ${step.uses}${cacheNote}`);
161
+ }
162
+ else {
163
+ result.matchedLines.push(`step: ${step.name ?? '(unnamed)'} with cache`);
164
+ }
165
+ }
166
+ }
167
+ return result;
168
+ }
169
+ // ---------------------------------------------------------------------------
170
+ // Factory
171
+ // ---------------------------------------------------------------------------
172
+ const PATTERN_LABEL = 'pull_request_target + checkout-head + cache-write';
173
+ /**
174
+ * Collect every string from a parsed workflow tree that an IOC pattern
175
+ * could meaningfully match: the full file text, every step's `uses`,
176
+ * `run`, and `name`. Each entry carries a label so emitted findings can
177
+ * cite where the match occurred.
178
+ */
179
+ function collectIocCandidates(workflowFileName, rawText, parsed) {
180
+ const out = [];
181
+ out.push({ label: `${workflowFileName} (full text)`, value: rawText });
182
+ const jobs = parsed.jobs;
183
+ if (!isRecord(jobs))
184
+ return out;
185
+ for (const [jobName, jobValue] of Object.entries(jobs)) {
186
+ if (!isRecord(jobValue))
187
+ continue;
188
+ const steps = jobValue.steps;
189
+ if (!Array.isArray(steps))
190
+ continue;
191
+ steps.forEach((raw, idx) => {
192
+ const step = asStep(raw);
193
+ if (!step)
194
+ return;
195
+ const stepLabel = `${workflowFileName}:${jobName}#${idx}`;
196
+ if (step.uses)
197
+ out.push({ label: `${stepLabel}.uses`, value: step.uses });
198
+ if (step.run)
199
+ out.push({ label: `${stepLabel}.run`, value: step.run });
200
+ if (step.name)
201
+ out.push({ label: `${stepLabel}.name`, value: step.name });
202
+ });
203
+ }
204
+ return out;
205
+ }
206
+ export function createWorkflowScanner(deps = defaultDeps) {
207
+ const loadFeed = deps.loadLocalIocFeed ?? loadLocalIocFeed;
208
+ return {
209
+ iocClass: 'workflow',
210
+ async scan(ctx) {
211
+ const files = await deps.readWorkflowsDir(ctx.repoPath);
212
+ const findings = [];
213
+ let parseErrors = 0;
214
+ const warnings = [];
215
+ // Org-local custom workflow IOCs. Resilient: a feed error is captured
216
+ // as a warning, never an exception — a missing feed must not abort
217
+ // the built-in pull_request_target check.
218
+ const iocEntries = await loadFeed(ctx.config.organizationId, 'workflow').catch((err) => {
219
+ warnings.push(`local-ioc-feed: ${err.message}`);
220
+ return [];
221
+ });
222
+ // Dedupe findings across the file's pattern match and the IOC scan
223
+ // when both fire for the same identifier shape.
224
+ const seen = new Set();
225
+ for (const file of files) {
226
+ let raw;
227
+ try {
228
+ raw = await deps.readFile(file);
229
+ }
230
+ catch {
231
+ parseErrors += 1;
232
+ continue;
233
+ }
234
+ let parsed;
235
+ try {
236
+ parsed = parseYaml(raw);
237
+ }
238
+ catch {
239
+ parseErrors += 1;
240
+ continue;
241
+ }
242
+ if (!isRecord(parsed))
243
+ continue;
244
+ const workflowFileName = baseName(file);
245
+ // Built-in pull_request_target + checkout-head + cache-write check.
246
+ if (hasPullRequestTargetTrigger(parsed.on)) {
247
+ const jobs = parsed.jobs;
248
+ if (isRecord(jobs)) {
249
+ for (const [jobName, jobValue] of Object.entries(jobs)) {
250
+ const analysis = analyzeJob(jobValue);
251
+ if (!analysis.hasCheckoutOfForkHead || !analysis.cacheWriteAfterCheckout)
252
+ continue;
253
+ const identifier = `${workflowFileName}:${jobName}`;
254
+ if (seen.has(identifier))
255
+ continue;
256
+ seen.add(identifier);
257
+ findings.push({
258
+ iocClass: 'workflow',
259
+ severity: 'high',
260
+ identifier,
261
+ payload: {
262
+ workflow: workflowFileName,
263
+ job: jobName,
264
+ pattern: PATTERN_LABEL,
265
+ matched_lines: analysis.matchedLines,
266
+ },
267
+ });
268
+ }
269
+ }
270
+ }
271
+ // Local IOC feed pass: test every entry's pattern against the file
272
+ // text and every step `uses`/`run`/`name` value. One finding per
273
+ // (entry, file) so multiple matches inside one file collapse into a
274
+ // single finding referencing the strongest match location.
275
+ if (iocEntries.length > 0) {
276
+ const candidates = collectIocCandidates(workflowFileName, raw, parsed);
277
+ for (const entry of iocEntries) {
278
+ if (entry.iocClass !== 'workflow')
279
+ continue;
280
+ const match = candidates.find((c) => matchesPattern(c.value, entry));
281
+ if (!match)
282
+ continue;
283
+ const identifier = `${workflowFileName}#ioc:${entry.id}`;
284
+ if (seen.has(identifier))
285
+ continue;
286
+ seen.add(identifier);
287
+ findings.push({
288
+ iocClass: 'workflow',
289
+ severity: entry.severity,
290
+ identifier,
291
+ payload: {
292
+ source: 'local_ioc_feed',
293
+ entry_id: entry.id,
294
+ pattern_type: entry.patternType,
295
+ pattern: entry.pattern,
296
+ advisory_text: entry.advisoryText,
297
+ workflow: workflowFileName,
298
+ matched_at: match.label,
299
+ },
300
+ });
301
+ }
302
+ }
303
+ }
304
+ const coverage = {
305
+ workflows_scanned: files.length,
306
+ risky_patterns: findings.length,
307
+ parse_errors: parseErrors,
308
+ local_ioc_entries: iocEntries.length,
309
+ };
310
+ if (warnings.length > 0) {
311
+ coverage.warnings = warnings;
312
+ }
313
+ return { findings, coverage };
314
+ },
315
+ };
316
+ }
317
+ function baseName(p) {
318
+ const slash = p.lastIndexOf('/');
319
+ return slash >= 0 ? p.slice(slash + 1) : p;
320
+ }
321
+ // ---------------------------------------------------------------------------
322
+ // Default scanner -- real fs IO
323
+ // ---------------------------------------------------------------------------
324
+ export const workflowScanner = createWorkflowScanner();
325
+ //# 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAM1D,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5C,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,mBAAmB,EAAE,QAAQ,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,oBAAoB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;QACnD,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,IAAI,CAAC,CAAC;IACV;;;OAGG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,cAAc,EAAE,MAAM,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClC;;OAEG;IACH,eAAe,EAAE,CAAC,KAAK,EAAE;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClC;;OAEG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;KACzB,KAAK,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtC,+DAA+D;IAC/D,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,kCAAkC;IAClC,UAAU,EAAE,CAAC,KAAK,EAAE;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,MAAM,EAAE,eAAe,GAAG,qBAAqB,CAAC;QAChD,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAaD,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,GAAG,OAAO,CAE/E;AAyCD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,UAAU,GAAG,yBAAyB,GAAG,wBAAwB,GAAG,oBAAoB,GAAG,2BAA2B,CAAC;IAC/H,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,uBAAuB,CAAC,CA4DlC;AAMD,wBAAgB,0BAA0B,IAAI,cAAc,CAyB3D"}