@telora/daemon 0.15.36 → 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/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/delivery-lifecycle.d.ts +3 -0
- package/dist/delivery-lifecycle.d.ts.map +1 -1
- package/dist/delivery-lifecycle.js +13 -1
- package/dist/delivery-lifecycle.js.map +1 -1
- package/dist/drift-eval-loop.d.ts +63 -0
- package/dist/drift-eval-loop.d.ts.map +1 -0
- package/dist/drift-eval-loop.js +215 -0
- package/dist/drift-eval-loop.js.map +1 -0
- package/dist/drift-evaluator.d.ts +51 -0
- package/dist/drift-evaluator.d.ts.map +1 -0
- package/dist/drift-evaluator.js +62 -0
- package/dist/drift-evaluator.js.map +1 -0
- 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-completion-event.d.ts +2 -0
- package/dist/focus-completion-event.d.ts.map +1 -1
- package/dist/focus-completion-event.js +51 -13
- package/dist/focus-completion-event.js.map +1 -1
- package/dist/focus-completion.d.ts +1 -1
- package/dist/focus-completion.js +7 -7
- package/dist/focus-completion.js.map +1 -1
- package/dist/focus-engine.d.ts.map +1 -1
- package/dist/focus-engine.js +71 -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/focus-loop.d.ts +1 -1
- package/dist/focus-loop.js +2 -2
- package/dist/focus-loop.js.map +1 -1
- package/dist/focus-phase.js +1 -1
- package/dist/focus-phase.js.map +1 -1
- package/dist/focus-prompt-builder.d.ts +1 -1
- package/dist/focus-prompt-builder.js +1 -1
- package/dist/listener-auto-advance.d.ts +3 -3
- package/dist/listener-auto-advance.js +10 -10
- package/dist/listener-auto-advance.js.map +1 -1
- package/dist/listener.js +1 -1
- package/dist/listener.js.map +1 -1
- package/dist/queries/deliveries.d.ts +20 -1
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js +8 -1
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/drift.d.ts +50 -0
- package/dist/queries/drift.d.ts.map +1 -0
- package/dist/queries/drift.js +28 -0
- package/dist/queries/drift.js.map +1 -0
- package/dist/queries/focuses.d.ts.map +1 -1
- package/dist/queries/focuses.js +1 -0
- package/dist/queries/focuses.js.map +1 -1
- package/dist/queries/index.d.ts +2 -0
- package/dist/queries/index.d.ts.map +1 -1
- package/dist/queries/index.js +1 -0
- package/dist/queries/index.js.map +1 -1
- package/dist/queries/schemas.d.ts +2 -0
- package/dist/queries/schemas.d.ts.map +1 -1
- package/dist/queries/schemas.js +1 -0
- package/dist/queries/schemas.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/dist/stage-classifier.d.ts +2 -2
- package/dist/stage-classifier.d.ts.map +1 -1
- package/dist/stage-classifier.js +7 -7
- package/dist/stage-classifier.js.map +1 -1
- package/dist/state-cascade.d.ts +1 -1
- package/dist/state-cascade.js +1 -1
- package/dist/team-prompt-base.d.ts.map +1 -1
- package/dist/team-prompt-base.js +15 -0
- package/dist/team-prompt-base.js.map +1 -1
- package/dist/types/focus.d.ts +6 -0
- package/dist/types/focus.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -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":"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"}
|
|
@@ -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"}
|