@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.
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 +88 -0
  5. package/dist/feeds/ghsa.d.ts.map +1 -0
  6. package/dist/feeds/ghsa.js +219 -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 +89 -0
  13. package/dist/feeds/osv.d.ts.map +1 -0
  14. package/dist/feeds/osv.js +266 -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 +40 -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 +34 -0
  32. package/dist/scanners/workflow.d.ts.map +1 -0
  33. package/dist/scanners/workflow.js +239 -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 +96 -0
  44. package/dist/security-scan-engine.d.ts.map +1 -0
  45. package/dist/security-scan-engine.js +189 -0
  46. package/dist/security-scan-engine.js.map +1 -0
  47. package/package.json +3 -2
@@ -0,0 +1,148 @@
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 { callApi } from './queries/shared.js';
26
+ // ---------------------------------------------------------------------------
27
+ // Severity comparison
28
+ // ---------------------------------------------------------------------------
29
+ const SEVERITY_RANK = {
30
+ low: 1,
31
+ medium: 2,
32
+ high: 3,
33
+ critical: 4,
34
+ };
35
+ /** Returns true when `severity` is at or above `threshold`. */
36
+ export function meetsThreshold(severity, threshold) {
37
+ return SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
38
+ }
39
+ function isSuppressedAndActive(finding, now = new Date()) {
40
+ if (finding.status !== 'suppressed')
41
+ return false;
42
+ if (!finding.suppression)
43
+ return false;
44
+ const s = finding.suppression;
45
+ if (!s.expires_at)
46
+ return true;
47
+ return new Date(s.expires_at).getTime() > now.getTime();
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Statement + label builders
51
+ // ---------------------------------------------------------------------------
52
+ function buildAdvisorySummary(finding) {
53
+ const payload = finding.payload;
54
+ const candidates = [
55
+ typeof payload.title === 'string' ? payload.title : null,
56
+ typeof payload.summary === 'string' ? payload.summary : null,
57
+ typeof payload.advisory_text === 'string' ? payload.advisory_text : null,
58
+ ].filter((s) => Boolean(s));
59
+ const head = candidates[0] ?? `Security finding ${finding.identifier}`;
60
+ return `${head} (${finding.iocClass}, severity=${finding.severity})`;
61
+ }
62
+ function buildEntityLabel(finding) {
63
+ return `${finding.iocClass}:${finding.identifier}`;
64
+ }
65
+ export async function processNewFinding(finding, options, deps) {
66
+ if (finding.linkedInjectionId)
67
+ return { status: 'skipped_already_linked' };
68
+ if (isSuppressedAndActive(finding))
69
+ return { status: 'skipped_suppressed' };
70
+ if (!options.force && !meetsThreshold(finding.severity, options.autoInjectThreshold)) {
71
+ return { status: 'skipped_below_threshold' };
72
+ }
73
+ const focus = await deps.resolveSecurityFocus(finding.productId);
74
+ if (!focus)
75
+ return { status: 'skipped_no_security_focus' };
76
+ const entity = await deps.upsertEntityNode({
77
+ treeId: focus.treeId,
78
+ organizationId: finding.organizationId,
79
+ label: buildEntityLabel(finding),
80
+ payload: { ioc_class: finding.iocClass, identifier: finding.identifier },
81
+ });
82
+ const injection = await deps.createInjection({
83
+ treeId: focus.treeId,
84
+ organizationId: finding.organizationId,
85
+ statement: buildAdvisorySummary(finding),
86
+ targetNodeId: entity.nodeId,
87
+ sourcePayload: {
88
+ finding_id: finding.id,
89
+ ioc_class: finding.iocClass,
90
+ severity: finding.severity,
91
+ identifier: finding.identifier,
92
+ },
93
+ });
94
+ const delivery = await deps.createDelivery({
95
+ focusId: focus.focusId,
96
+ productId: finding.productId,
97
+ organizationId: finding.organizationId,
98
+ name: `Remediate ${buildEntityLabel(finding)}`,
99
+ description: buildAdvisorySummary(finding),
100
+ injectionNodeId: injection.nodeId,
101
+ });
102
+ await deps.linkFinding(finding.id, injection.nodeId);
103
+ await deps.writeAudit({
104
+ findingId: finding.id,
105
+ organizationId: finding.organizationId,
106
+ action: options.force ? 'manually_remediated' : 'auto_injected',
107
+ actorUserId: options.actorUserId,
108
+ reason: options.force ? 'click_to_remediate' : 'severity_threshold_met',
109
+ payload: {
110
+ severity: finding.severity,
111
+ threshold: options.autoInjectThreshold,
112
+ delivery_id: delivery.deliveryId,
113
+ injection_node_id: injection.nodeId,
114
+ },
115
+ });
116
+ return {
117
+ status: 'injected',
118
+ injectionNodeId: injection.nodeId,
119
+ deliveryId: delivery.deliveryId,
120
+ };
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Default Deps -- daemon-side wiring via callApi.
124
+ // ---------------------------------------------------------------------------
125
+ export function buildDefaultAutoInjectDeps() {
126
+ return {
127
+ resolveSecurityFocus: async (productId) => {
128
+ const res = await callApi('daemon_resolve_security_focus', { productId });
129
+ return res?.focus ?? null;
130
+ },
131
+ upsertEntityNode: async (input) => {
132
+ return callApi('daemon_upsert_security_entity_node', input);
133
+ },
134
+ createInjection: async (input) => {
135
+ return callApi('daemon_create_security_injection', input);
136
+ },
137
+ createDelivery: async (input) => {
138
+ return callApi('daemon_create_security_delivery', input);
139
+ },
140
+ linkFinding: async (findingId, injectionNodeId) => {
141
+ await callApi('daemon_link_finding_to_injection', { findingId, injectionNodeId });
142
+ },
143
+ writeAudit: async (input) => {
144
+ await callApi('daemon_write_security_finding_audit', input);
145
+ },
146
+ };
147
+ }
148
+ //# sourceMappingURL=security-auto-inject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-auto-inject.js","sourceRoot":"","sources":["../src/security-auto-inject.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Re-scan resolution: when a scan run completes that covered a finding's
3
+ * IOC class but did not re-produce a previously-open finding, flip the
4
+ * finding to 'resolved' and (if the linked injection's delivery is done)
5
+ * auto-verify the injection.
6
+ *
7
+ * Called by the scanner engine immediately after a run finishes writing
8
+ * its findings, before suppression-expiry sweeps and before the next
9
+ * tick begins.
10
+ *
11
+ * @module security-rescan-resolution
12
+ */
13
+ export interface OpenFindingRow {
14
+ id: string;
15
+ organizationId: string;
16
+ productId: string;
17
+ iocClass: string;
18
+ identifier: string;
19
+ linkedInjectionId: string | null;
20
+ }
21
+ export interface ResolutionDeps {
22
+ /**
23
+ * Find all 'open' findings for the product that match the IOC classes
24
+ * the just-completed scan run covered. The engine knows which classes
25
+ * ran so the caller filters down to those.
26
+ */
27
+ listOpenFindings: (productId: string, iocClasses: string[]) => Promise<OpenFindingRow[]>;
28
+ /** Set the finding to 'resolved' with resolved_at = now(). */
29
+ markFindingResolved: (findingId: string) => Promise<void>;
30
+ /** Append audit row for the resolution. */
31
+ writeAudit: (input: {
32
+ findingId: string;
33
+ organizationId: string;
34
+ action: 'resolved';
35
+ payload: Record<string, unknown>;
36
+ }) => Promise<void>;
37
+ /**
38
+ * Returns the execution_status of the delivery linked to the injection,
39
+ * or null if there is no such delivery.
40
+ */
41
+ getInjectionDeliveryStatus: (injectionNodeId: string) => Promise<string | null>;
42
+ /** Compound op: retire the injection + promote FRT overlays to CRT. */
43
+ verifyInjection: (injectionNodeId: string) => Promise<void>;
44
+ }
45
+ /**
46
+ * Map of identifiers that the current run did emit, per IOC class.
47
+ */
48
+ export interface ScanRunFindingSet {
49
+ iocClass: string;
50
+ identifiers: Set<string>;
51
+ }
52
+ /**
53
+ * Resolve previously-open findings that didn't re-appear in this run.
54
+ *
55
+ * @returns the list of finding ids that were flipped to 'resolved'.
56
+ */
57
+ export declare function resolveStaleFindings(productId: string, observedSets: ScanRunFindingSet[], deps: ResolutionDeps): Promise<string[]>;
58
+ export declare function buildDefaultResolutionDeps(): ResolutionDeps;
59
+ export interface SuppressionExpirySweepDeps {
60
+ /** Returns findings where status='suppressed' and suppression.expires_at < now(). */
61
+ listExpiredSuppressions: () => Promise<Array<{
62
+ id: string;
63
+ organizationId: string;
64
+ }>>;
65
+ /** Set status='open', suppression=null. */
66
+ unsuppressFinding: (findingId: string) => Promise<void>;
67
+ /** Append audit row with action='unsuppressed' and reason='suppression_expired'. */
68
+ writeAudit: (input: {
69
+ findingId: string;
70
+ organizationId: string;
71
+ action: 'unsuppressed';
72
+ payload: Record<string, unknown>;
73
+ }) => Promise<void>;
74
+ }
75
+ /**
76
+ * Sweep suppressions whose expires_at has passed and flip them back to
77
+ * 'open'. Idempotent: a suppression already expired is a no-op on the
78
+ * second call because the predicate now matches status='open'.
79
+ *
80
+ * @returns the finding ids whose suppression was lifted.
81
+ */
82
+ export declare function runSuppressionExpirySweep(deps: SuppressionExpirySweepDeps): Promise<string[]>;
83
+ export declare function buildDefaultSuppressionExpirySweepDeps(): SuppressionExpirySweepDeps;
84
+ //# sourceMappingURL=security-rescan-resolution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-rescan-resolution.d.ts","sourceRoot":"","sources":["../src/security-rescan-resolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IACzF,8DAA8D;IAC9D,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,2CAA2C;IAC3C,UAAU,EAAE,CAAC,KAAK,EAAE;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,MAAM,EAAE,UAAU,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;;OAGG;IACH,0BAA0B,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChF,uEAAuE;IACvE,eAAe,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,iBAAiB,EAAE,EACjC,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAqCnB;AAMD,wBAAgB,0BAA0B,IAAI,cAAc,CA0B3D;AAMD,MAAM,WAAW,0BAA0B;IACzC,qFAAqF;IACrF,uBAAuB,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACtF,2CAA2C;IAC3C,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,oFAAoF;IACpF,UAAU,EAAE,CAAC,KAAK,EAAE;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,MAAM,EAAE,cAAc,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC,CAcnB;AAED,wBAAgB,sCAAsC,IAAI,0BAA0B,CAgBnF"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Re-scan resolution: when a scan run completes that covered a finding's
3
+ * IOC class but did not re-produce a previously-open finding, flip the
4
+ * finding to 'resolved' and (if the linked injection's delivery is done)
5
+ * auto-verify the injection.
6
+ *
7
+ * Called by the scanner engine immediately after a run finishes writing
8
+ * its findings, before suppression-expiry sweeps and before the next
9
+ * tick begins.
10
+ *
11
+ * @module security-rescan-resolution
12
+ */
13
+ import { callApi } from './queries/shared.js';
14
+ /**
15
+ * Resolve previously-open findings that didn't re-appear in this run.
16
+ *
17
+ * @returns the list of finding ids that were flipped to 'resolved'.
18
+ */
19
+ export async function resolveStaleFindings(productId, observedSets, deps) {
20
+ const iocClasses = observedSets.map((s) => s.iocClass);
21
+ if (iocClasses.length === 0)
22
+ return [];
23
+ const openFindings = await deps.listOpenFindings(productId, iocClasses);
24
+ const observedByClass = new Map();
25
+ for (const set of observedSets) {
26
+ observedByClass.set(set.iocClass, set.identifiers);
27
+ }
28
+ const resolvedIds = [];
29
+ for (const finding of openFindings) {
30
+ const observedInClass = observedByClass.get(finding.iocClass);
31
+ if (!observedInClass)
32
+ continue;
33
+ if (observedInClass.has(finding.identifier))
34
+ continue;
35
+ // Finding was open, its class was covered, but it didn't re-appear.
36
+ await deps.markFindingResolved(finding.id);
37
+ await deps.writeAudit({
38
+ findingId: finding.id,
39
+ organizationId: finding.organizationId,
40
+ action: 'resolved',
41
+ payload: { reason: 'absent_in_rescan', ioc_class: finding.iocClass },
42
+ });
43
+ resolvedIds.push(finding.id);
44
+ // Auto-verify the linked injection if its delivery has reached 'done'.
45
+ if (finding.linkedInjectionId) {
46
+ const status = await deps.getInjectionDeliveryStatus(finding.linkedInjectionId);
47
+ if (status === 'done') {
48
+ await deps.verifyInjection(finding.linkedInjectionId);
49
+ }
50
+ }
51
+ }
52
+ return resolvedIds;
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Default Deps -- daemon-side wiring via callApi.
56
+ // ---------------------------------------------------------------------------
57
+ export function buildDefaultResolutionDeps() {
58
+ return {
59
+ listOpenFindings: async (productId, iocClasses) => {
60
+ const res = await callApi('daemon_list_open_security_findings', { productId, iocClasses });
61
+ return res.items ?? [];
62
+ },
63
+ markFindingResolved: async (findingId) => {
64
+ await callApi('daemon_resolve_security_finding', { findingId });
65
+ },
66
+ writeAudit: async (input) => {
67
+ await callApi('daemon_write_security_finding_audit', input);
68
+ },
69
+ getInjectionDeliveryStatus: async (injectionNodeId) => {
70
+ const res = await callApi('daemon_get_injection_delivery_status', { injectionNodeId });
71
+ return res.status;
72
+ },
73
+ verifyInjection: async (injectionNodeId) => {
74
+ await callApi('reality_tree_verify_injection', { injectionNodeId });
75
+ },
76
+ };
77
+ }
78
+ /**
79
+ * Sweep suppressions whose expires_at has passed and flip them back to
80
+ * 'open'. Idempotent: a suppression already expired is a no-op on the
81
+ * second call because the predicate now matches status='open'.
82
+ *
83
+ * @returns the finding ids whose suppression was lifted.
84
+ */
85
+ export async function runSuppressionExpirySweep(deps) {
86
+ const expired = await deps.listExpiredSuppressions();
87
+ const lifted = [];
88
+ for (const finding of expired) {
89
+ await deps.unsuppressFinding(finding.id);
90
+ await deps.writeAudit({
91
+ findingId: finding.id,
92
+ organizationId: finding.organizationId,
93
+ action: 'unsuppressed',
94
+ payload: { reason: 'suppression_expired' },
95
+ });
96
+ lifted.push(finding.id);
97
+ }
98
+ return lifted;
99
+ }
100
+ export function buildDefaultSuppressionExpirySweepDeps() {
101
+ return {
102
+ listExpiredSuppressions: async () => {
103
+ const res = await callApi('daemon_list_expired_security_suppressions', {});
104
+ return res.items ?? [];
105
+ },
106
+ unsuppressFinding: async (findingId) => {
107
+ await callApi('daemon_unsuppress_security_finding', { findingId });
108
+ },
109
+ writeAudit: async (input) => {
110
+ await callApi('daemon_write_security_finding_audit', input);
111
+ },
112
+ };
113
+ }
114
+ //# sourceMappingURL=security-rescan-resolution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-rescan-resolution.js","sourceRoot":"","sources":["../src/security-rescan-resolution.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Security scan engine.
3
+ *
4
+ * Polls security_scan_configs for due scans (cron-due or manual_run_requested_at
5
+ * set), dispatches pluggable Scanner implementations per IOC class, writes
6
+ * one security_scan_runs row plus N security_findings rows per execution,
7
+ * and (downstream) hands findings to security-auto-inject for severity-gated
8
+ * injection materialization.
9
+ *
10
+ * Activation: gated by shouldRunLoop('TELORA_SECURITY_SCAN_LOOP') in
11
+ * unified-shell.ts. Opt-out semantics match the other daemon loop ticks
12
+ * (unset/anything-but-'0' = enabled, '0' = disabled). See
13
+ * docs/runbook-loop-activation.md.
14
+ *
15
+ * Pattern reference: verification-engine.ts (pluggable strategies + Deps).
16
+ *
17
+ * @module security-scan-engine
18
+ */
19
+ import type { DaemonConfig } from './types.js';
20
+ import { type AutoInjectDeps } from './security-auto-inject.js';
21
+ import { type ResolutionDeps } from './security-rescan-resolution.js';
22
+ export type Severity = 'low' | 'medium' | 'high' | 'critical';
23
+ /** Configuration row driving an individual product's scan cadence. */
24
+ export interface ScanConfig {
25
+ id: string;
26
+ organizationId: string;
27
+ productId: string;
28
+ scheduleCron: string;
29
+ enabledIocClasses: string[];
30
+ autoInjectSeverityThreshold: Severity;
31
+ enabled: boolean;
32
+ manualRunRequestedAt: string | null;
33
+ lastRunAt: string | null;
34
+ }
35
+ /** Per-scan context passed to each Scanner. */
36
+ export interface ScanContext {
37
+ config: ScanConfig;
38
+ repoPath: string;
39
+ }
40
+ /** Single finding draft emitted by a Scanner. Stored in security_findings. */
41
+ export interface FindingDraft {
42
+ iocClass: string;
43
+ severity: Severity;
44
+ identifier: string;
45
+ payload: Record<string, unknown>;
46
+ }
47
+ /** Per-scanner output for a single scan run. */
48
+ export interface ScanResult {
49
+ findings: FindingDraft[];
50
+ /** Coverage breadcrumb (e.g. packages_audited, files_scanned) + any warnings. */
51
+ coverage: Record<string, unknown>;
52
+ }
53
+ /** Pluggable Scanner contract -- one per IOC class. */
54
+ export interface Scanner {
55
+ /** IOC class slug, matched against ScanConfig.enabledIocClasses. */
56
+ iocClass: string;
57
+ scan(ctx: ScanContext): Promise<ScanResult>;
58
+ }
59
+ export declare function registerScanner(scanner: Scanner): void;
60
+ export declare function getRegisteredScanners(): Scanner[];
61
+ export interface SecurityScanDeps {
62
+ getDueScanConfigs: () => Promise<ScanConfig[]>;
63
+ startRun: (configId: string, trigger: 'schedule' | 'manual') => Promise<string>;
64
+ finishRun: (runId: string, update: {
65
+ status: 'succeeded' | 'failed' | 'partial';
66
+ coverageSummary: Record<string, unknown>;
67
+ findingsCountBySeverity: Record<Severity, number>;
68
+ durationMs: number;
69
+ }) => Promise<void>;
70
+ /**
71
+ * Persist a finding and return its DB-assigned id so downstream
72
+ * hooks (auto-injection, resolution) can reference it.
73
+ */
74
+ writeFinding: (runId: string, productId: string, organizationId: string, finding: FindingDraft) => Promise<{
75
+ findingId: string;
76
+ }>;
77
+ clearManualRunRequest: (configId: string) => Promise<void>;
78
+ resolveCwd: (productId: string) => string;
79
+ scanners: Scanner[];
80
+ /**
81
+ * Optional severity-gated auto-injection hook. When set, each newly
82
+ * written finding is passed to processNewFinding so the daemon can
83
+ * materialize a remediation injection + delivery for it.
84
+ */
85
+ autoInjectDeps?: AutoInjectDeps;
86
+ /**
87
+ * Optional re-scan resolution hook. When set, after every scan run
88
+ * finishes, the engine asks the resolution module to flip previously
89
+ * open findings whose identifiers did not re-appear to 'resolved'.
90
+ */
91
+ resolutionDeps?: ResolutionDeps;
92
+ }
93
+ export declare function runScanForConfig(config: ScanConfig, trigger: 'schedule' | 'manual', deps: SecurityScanDeps): Promise<void>;
94
+ export declare function runSecurityScanTick(deps: SecurityScanDeps): Promise<void>;
95
+ export declare function buildDefaultSecurityScanDeps(config: DaemonConfig): SecurityScanDeps;
96
+ //# sourceMappingURL=security-scan-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-scan-engine.d.ts","sourceRoot":"","sources":["../src/security-scan-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Security scan engine.
3
+ *
4
+ * Polls security_scan_configs for due scans (cron-due or manual_run_requested_at
5
+ * set), dispatches pluggable Scanner implementations per IOC class, writes
6
+ * one security_scan_runs row plus N security_findings rows per execution,
7
+ * and (downstream) hands findings to security-auto-inject for severity-gated
8
+ * injection materialization.
9
+ *
10
+ * Activation: gated by shouldRunLoop('TELORA_SECURITY_SCAN_LOOP') in
11
+ * unified-shell.ts. Opt-out semantics match the other daemon loop ticks
12
+ * (unset/anything-but-'0' = enabled, '0' = disabled). See
13
+ * docs/runbook-loop-activation.md.
14
+ *
15
+ * Pattern reference: verification-engine.ts (pluggable strategies + Deps).
16
+ *
17
+ * @module security-scan-engine
18
+ */
19
+ import { callApi } from './queries/shared.js';
20
+ import { configForProduct } from './config.js';
21
+ import { buildDefaultAutoInjectDeps, processNewFinding, } from './security-auto-inject.js';
22
+ import { resolveStaleFindings, buildDefaultResolutionDeps, } from './security-rescan-resolution.js';
23
+ // ---------------------------------------------------------------------------
24
+ // Default registry -- scanners self-register here in their own modules
25
+ // ---------------------------------------------------------------------------
26
+ const DEFAULT_REGISTRY = new Map();
27
+ export function registerScanner(scanner) {
28
+ DEFAULT_REGISTRY.set(scanner.iocClass, scanner);
29
+ }
30
+ export function getRegisteredScanners() {
31
+ return [...DEFAULT_REGISTRY.values()];
32
+ }
33
+ export async function runScanForConfig(config, trigger, deps) {
34
+ const runId = await deps.startRun(config.id, trigger);
35
+ const startedAt = Date.now();
36
+ const coverage = {};
37
+ const warnings = [];
38
+ const counts = { low: 0, medium: 0, high: 0, critical: 0 };
39
+ let anyFailure = false;
40
+ let anySuccess = false;
41
+ const enabledScanners = deps.scanners.filter((s) => config.enabledIocClasses.includes(s.iocClass));
42
+ // Per-class observed identifier sets for re-scan resolution. Only
43
+ // classes whose scanner ran without error contribute -- a failed
44
+ // scanner can't claim coverage for its class.
45
+ const observedByClass = new Map();
46
+ for (const scanner of enabledScanners) {
47
+ try {
48
+ const result = await scanner.scan({
49
+ config,
50
+ repoPath: deps.resolveCwd(config.productId),
51
+ });
52
+ coverage[scanner.iocClass] = result.coverage;
53
+ anySuccess = true;
54
+ const observed = new Set();
55
+ for (const finding of result.findings) {
56
+ const { findingId } = await deps.writeFinding(runId, config.productId, config.organizationId, finding);
57
+ counts[finding.severity] = (counts[finding.severity] ?? 0) + 1;
58
+ observed.add(finding.identifier);
59
+ // Severity-gated auto-injection. Run per-finding so a failing
60
+ // injection for one finding does not block the others.
61
+ if (deps.autoInjectDeps) {
62
+ try {
63
+ const forInjection = {
64
+ id: findingId,
65
+ organizationId: config.organizationId,
66
+ productId: config.productId,
67
+ iocClass: finding.iocClass,
68
+ severity: finding.severity,
69
+ identifier: finding.identifier,
70
+ payload: finding.payload,
71
+ status: 'open',
72
+ suppression: null,
73
+ linkedInjectionId: null,
74
+ };
75
+ const options = {
76
+ autoInjectThreshold: config.autoInjectSeverityThreshold,
77
+ };
78
+ await processNewFinding(forInjection, options, deps.autoInjectDeps);
79
+ }
80
+ catch (err) {
81
+ warnings.push(`auto-inject ${finding.identifier}: ${err.message}`);
82
+ }
83
+ }
84
+ }
85
+ observedByClass.set(scanner.iocClass, observed);
86
+ }
87
+ catch (err) {
88
+ anyFailure = true;
89
+ warnings.push(`${scanner.iocClass}: ${err.message}`);
90
+ coverage[scanner.iocClass] = { error: err.message };
91
+ }
92
+ }
93
+ // Re-scan resolution: previously-open findings whose class was
94
+ // covered by this run but whose identifier did not re-appear are
95
+ // flipped to 'resolved'. Failures here are non-fatal -- the run
96
+ // already succeeded for its primary purpose (finding fresh issues).
97
+ if (deps.resolutionDeps && observedByClass.size > 0) {
98
+ try {
99
+ const observedSets = Array.from(observedByClass.entries()).map(([iocClass, identifiers]) => ({ iocClass, identifiers }));
100
+ const resolved = await resolveStaleFindings(config.productId, observedSets, deps.resolutionDeps);
101
+ if (resolved.length > 0) {
102
+ coverage.resolved_findings = resolved.length;
103
+ }
104
+ }
105
+ catch (err) {
106
+ warnings.push(`resolution: ${err.message}`);
107
+ }
108
+ }
109
+ if (warnings.length > 0) {
110
+ coverage.warnings = warnings;
111
+ }
112
+ const status = anyFailure
113
+ ? anySuccess
114
+ ? 'partial'
115
+ : 'failed'
116
+ : 'succeeded';
117
+ await deps.finishRun(runId, {
118
+ status,
119
+ coverageSummary: coverage,
120
+ findingsCountBySeverity: counts,
121
+ durationMs: Date.now() - startedAt,
122
+ });
123
+ if (trigger === 'manual') {
124
+ await deps.clearManualRunRequest(config.id);
125
+ }
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Loop tick -- invoked by unified-shell on a fixed cadence
129
+ // ---------------------------------------------------------------------------
130
+ export async function runSecurityScanTick(deps) {
131
+ const configs = await deps.getDueScanConfigs();
132
+ for (const config of configs) {
133
+ if (!config.enabled)
134
+ continue;
135
+ const trigger = config.manualRunRequestedAt ? 'manual' : 'schedule';
136
+ try {
137
+ await runScanForConfig(config, trigger, deps);
138
+ }
139
+ catch {
140
+ // Per-config failures are swallowed so a single broken product
141
+ // does not stop the engine from servicing others. The run row
142
+ // already records the failure status.
143
+ }
144
+ }
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Default Deps -- daemon-side wiring via callApi
148
+ // ---------------------------------------------------------------------------
149
+ export function buildDefaultSecurityScanDeps(config) {
150
+ const resolveCwd = (productId) => {
151
+ const product = config.products.find((p) => p.id === productId);
152
+ if (!product)
153
+ return config.repoPath;
154
+ return configForProduct(config, product).repoPath;
155
+ };
156
+ return {
157
+ getDueScanConfigs: async () => {
158
+ const res = await callApi('daemon_get_due_security_scan_configs', {});
159
+ return res.items ?? [];
160
+ },
161
+ startRun: async (configId, trigger) => {
162
+ const res = await callApi('daemon_start_security_scan_run', {
163
+ configId,
164
+ trigger,
165
+ });
166
+ return res.runId;
167
+ },
168
+ finishRun: async (runId, update) => {
169
+ await callApi('daemon_finish_security_scan_run', { runId, ...update });
170
+ },
171
+ writeFinding: async (runId, productId, organizationId, finding) => {
172
+ const res = await callApi('daemon_write_security_finding', {
173
+ runId,
174
+ productId,
175
+ organizationId,
176
+ ...finding,
177
+ });
178
+ return { findingId: res.findingId };
179
+ },
180
+ clearManualRunRequest: async (configId) => {
181
+ await callApi('daemon_clear_manual_scan_request', { configId });
182
+ },
183
+ resolveCwd,
184
+ scanners: getRegisteredScanners(),
185
+ autoInjectDeps: buildDefaultAutoInjectDeps(),
186
+ resolutionDeps: buildDefaultResolutionDeps(),
187
+ };
188
+ }
189
+ //# sourceMappingURL=security-scan-engine.js.map