@clear-capabilities/agentic-security-scanner 0.172.1 → 0.173.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +135 -11
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +27 -27
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +28 -3
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +2 -0
- package/src/context-intelligence/config.js +3 -1
- package/src/context-intelligence/controls.js +49 -1
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +2 -2
- package/src/context-intelligence/review.js +68 -4
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
export const id = 3868;
|
|
2
|
+
export const ids = [3868];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 33868:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ MEMORY_PROFILES: () => (/* binding */ MEMORY_PROFILES),
|
|
10
|
+
/* harmony export */ capabilitiesFromFamilyHint: () => (/* binding */ capabilitiesFromFamilyHint),
|
|
11
|
+
/* harmony export */ classifyModelFamily: () => (/* binding */ classifyModelFamily),
|
|
12
|
+
/* harmony export */ detectMemoryTier: () => (/* binding */ detectMemoryTier),
|
|
13
|
+
/* harmony export */ detectSystemMemory: () => (/* binding */ detectSystemMemory),
|
|
14
|
+
/* harmony export */ recommendAdmission: () => (/* binding */ recommendAdmission)
|
|
15
|
+
/* harmony export */ });
|
|
16
|
+
/* unused harmony exports KNOWN_MODEL_SIZE_GB, evaluateMemoryAdmission */
|
|
17
|
+
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(48161);
|
|
18
|
+
/* harmony import */ var _oom_feedback_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(67181);
|
|
19
|
+
// Model family hints, RAM-aware memory profiles, and the memory-admission
|
|
20
|
+
// check for the Ollama provider (agentic-security-ollama-offline-prd.md
|
|
21
|
+
// §13, §14, §15, §22.3, §30).
|
|
22
|
+
//
|
|
23
|
+
// FAMILY HINTS ARE DEFAULTS, NEVER AUTHORITY (PRD §12/§13). A name like
|
|
24
|
+
// `gemma4:e2b` tells us nothing Ollama itself won't confirm — it only lets the
|
|
25
|
+
// harness suggest a sane default before any network call. If a model actually
|
|
26
|
+
// installed under a family-hinted name lacks a capability the hint implied,
|
|
27
|
+
// the runtime probe (model-probe.js, added when tool-calling/structured-output
|
|
28
|
+
// probing lands) always wins. This module only classifies and estimates; it
|
|
29
|
+
// never asserts a capability is present.
|
|
30
|
+
//
|
|
31
|
+
// MEMORY NUMBERS ARE ESTIMATES, NOT PROMISES (PRD §22.3, §14.1). Ollama
|
|
32
|
+
// artifact sizes and this module's headroom reserves are best-effort figures
|
|
33
|
+
// sourced from what Ollama currently publishes; they exist so the harness can
|
|
34
|
+
// fail BEFORE an OS-level OOM, not so it can claim an exact answer. Every
|
|
35
|
+
// admission decision leaves a stated safety margin rather than trying to pack
|
|
36
|
+
// memory to the byte.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// PRD §12/§13 FR-1203 — non-authoritative family hint from a model name.
|
|
42
|
+
// Longest/most-specific pattern first so `qwen3.5:4b` doesn't fall through to
|
|
43
|
+
// the bare `qwen` bucket.
|
|
44
|
+
const FAMILY_PATTERNS = [
|
|
45
|
+
[/^qwen3\.5/i, 'qwen3.5'],
|
|
46
|
+
[/^qwen3-coder-next/i, 'qwen3-coder-next'],
|
|
47
|
+
[/^qwen3-coder/i, 'qwen3-coder'],
|
|
48
|
+
[/^qwen2\.5-coder/i, 'qwen2.5-coder'],
|
|
49
|
+
[/^qwen3/i, 'qwen3'],
|
|
50
|
+
[/^qwen/i, 'qwen'],
|
|
51
|
+
[/^gemma4/i, 'gemma4'],
|
|
52
|
+
[/^functiongemma/i, 'functiongemma'],
|
|
53
|
+
[/^gemma3/i, 'gemma3'],
|
|
54
|
+
[/^gemma/i, 'gemma'],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** Non-authoritative family classification for defaults/messaging only. */
|
|
58
|
+
function classifyModelFamily(modelName) {
|
|
59
|
+
const name = String(modelName || '').trim();
|
|
60
|
+
for (const [re, family] of FAMILY_PATTERNS) if (re.test(name)) return family;
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// PRD §13.1 — non-authoritative defaults per family, overridden by any real
|
|
65
|
+
// runtime probe result (model-probe.js). `tools`/`structuredJson`/`thinking`
|
|
66
|
+
// are 'unknown' where Ollama's own behavior varies by specific tag/quant
|
|
67
|
+
// rather than by family alone.
|
|
68
|
+
const FAMILY_CAPABILITY_HINTS = {
|
|
69
|
+
'qwen3.5': { chat: true, structuredJson: true, tools: true, thinking: 'unknown' },
|
|
70
|
+
qwen3: { chat: true, structuredJson: true, tools: true, thinking: 'unknown' },
|
|
71
|
+
'qwen3-coder': { chat: true, structuredJson: true, tools: true, thinking: false },
|
|
72
|
+
'qwen3-coder-next': { chat: true, structuredJson: true, tools: true, thinking: false },
|
|
73
|
+
'qwen2.5-coder': { chat: true, structuredJson: true, tools: 'unknown', thinking: false },
|
|
74
|
+
qwen: { chat: true, structuredJson: 'unknown', tools: 'unknown', thinking: 'unknown' },
|
|
75
|
+
gemma4: { chat: true, structuredJson: true, tools: true, thinking: 'unknown' },
|
|
76
|
+
functiongemma: { chat: true, structuredJson: 'unknown', tools: true, thinking: false },
|
|
77
|
+
gemma3: { chat: true, structuredJson: true, tools: false, thinking: false },
|
|
78
|
+
gemma: { chat: true, structuredJson: 'unknown', tools: 'unknown', thinking: 'unknown' },
|
|
79
|
+
unknown: { chat: true, structuredJson: 'unknown', tools: 'unknown', thinking: 'unknown' },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the PRD §13.1 ModelCapabilities object from a family hint alone
|
|
84
|
+
* (Layer B). Layer A (Ollama's own /api/show metadata) and Layer C (runtime
|
|
85
|
+
* probes) are applied by the caller and override these fields — this
|
|
86
|
+
* function only ever sets `source.familyHint: true`.
|
|
87
|
+
*/
|
|
88
|
+
function capabilitiesFromFamilyHint(modelName) {
|
|
89
|
+
const family = classifyModelFamily(modelName);
|
|
90
|
+
const hint = FAMILY_CAPABILITY_HINTS[family] || FAMILY_CAPABILITY_HINTS.unknown;
|
|
91
|
+
return {
|
|
92
|
+
chat: hint.chat,
|
|
93
|
+
structuredJson: hint.structuredJson,
|
|
94
|
+
tools: hint.tools,
|
|
95
|
+
thinking: hint.thinking,
|
|
96
|
+
vision: false,
|
|
97
|
+
contextTokens: undefined,
|
|
98
|
+
source: { metadata: false, familyHint: true, runtimeProbe: false },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── RAM-aware memory profiles (PRD §14.4, §15.2, §22.3, §30) ───────────────
|
|
103
|
+
|
|
104
|
+
const MB = 1024 * 1024;
|
|
105
|
+
const GB = 1024 * MB;
|
|
106
|
+
|
|
107
|
+
// Best-effort artifact sizes as currently distributed by Ollama, used only to
|
|
108
|
+
// pick a SENSIBLE STARTING recommendation — the real admission decision below
|
|
109
|
+
// uses actually-free memory, not this table. Keep in sync with the PRD's own
|
|
110
|
+
// cited figures; a stale entry only affects the suggested default, never the
|
|
111
|
+
// admission math (which reads real os.freemem()).
|
|
112
|
+
const KNOWN_MODEL_SIZE_GB = Object.freeze({
|
|
113
|
+
'qwen3.5:2b': 1.7,
|
|
114
|
+
'qwen3.5:4b': 3.4,
|
|
115
|
+
'qwen3.5:9b': 6.6,
|
|
116
|
+
'gemma4:e2b': 7.2,
|
|
117
|
+
'gemma4:12b': 7.6,
|
|
118
|
+
'gemma4:latest': 9.6,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** PRD §30 profile presets. `auto` picks between these by detected RAM. */
|
|
122
|
+
const MEMORY_PROFILES = Object.freeze({
|
|
123
|
+
'8gb': {
|
|
124
|
+
label: '8gb',
|
|
125
|
+
preferredModel: 'qwen3.5:4b',
|
|
126
|
+
fallbackModel: 'qwen3.5:2b',
|
|
127
|
+
initialContextTokens: 4096,
|
|
128
|
+
targetContextTokens: 8192,
|
|
129
|
+
maxConcurrency: 1,
|
|
130
|
+
minFreeRamMb: 1536,
|
|
131
|
+
},
|
|
132
|
+
'16gb-qwen': {
|
|
133
|
+
label: '16gb-qwen',
|
|
134
|
+
preferredModel: 'qwen3.5:9b',
|
|
135
|
+
fallbackModel: 'qwen3.5:4b',
|
|
136
|
+
initialContextTokens: 16384,
|
|
137
|
+
targetContextTokens: 32768,
|
|
138
|
+
maxConcurrency: 1,
|
|
139
|
+
minFreeRamMb: 2048,
|
|
140
|
+
},
|
|
141
|
+
'16gb-gemma': {
|
|
142
|
+
label: '16gb-gemma',
|
|
143
|
+
preferredModel: 'gemma4:e2b',
|
|
144
|
+
fallbackModel: 'qwen3.5:4b',
|
|
145
|
+
initialContextTokens: 8192,
|
|
146
|
+
targetContextTokens: 16384,
|
|
147
|
+
maxConcurrency: 1,
|
|
148
|
+
minFreeRamMb: 2048,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* PRD §22.3 — detect total/available system RAM. Thin wrapper over `os` so
|
|
154
|
+
* tests can inject fake values without mocking the `os` module globally.
|
|
155
|
+
*/
|
|
156
|
+
function detectSystemMemory({ totalBytes, freeBytes } = {}) {
|
|
157
|
+
return {
|
|
158
|
+
totalBytes: Number.isFinite(totalBytes) ? totalBytes : node_os__WEBPACK_IMPORTED_MODULE_0__.totalmem(),
|
|
159
|
+
freeBytes: Number.isFinite(freeBytes) ? freeBytes : node_os__WEBPACK_IMPORTED_MODULE_0__.freemem(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Pick the RAM tier ('8gb' | '16gb') a machine falls into. Anything under
|
|
165
|
+
* ~9 GB total is treated as the 8 GB tier — real "8 GB" machines report
|
|
166
|
+
* slightly less than 8*1024^3 bytes to userspace (firmware/GPU reservations),
|
|
167
|
+
* so a hard `< 8*GB` cutoff would misclassify real 8 GB hardware as unknown.
|
|
168
|
+
*/
|
|
169
|
+
function detectMemoryTier(totalBytes) {
|
|
170
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0) return 'unknown';
|
|
171
|
+
if (totalBytes < 9 * GB) return '8gb';
|
|
172
|
+
return '16gb';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* PRD §22.3 admission algorithm: does `contextTokens` at `model` fit in
|
|
177
|
+
* currently-free memory with the configured reserve intact?
|
|
178
|
+
*
|
|
179
|
+
* This is deliberately conservative and coarse (PRD "avoid pretending memory
|
|
180
|
+
* estimates are exact"): model residency is estimated from KNOWN_MODEL_SIZE_GB
|
|
181
|
+
* when available (falling back to a pessimistic 8 GB assumption for an
|
|
182
|
+
* unrecognized tag so an unknown model never LOOKS safer than a known large
|
|
183
|
+
* one), and KV-cache growth is approximated as a fixed per-1K-token cost
|
|
184
|
+
* rather than modeled per-architecture — real KV cache size depends on layer
|
|
185
|
+
* count/head count/quantization the harness cannot know without Ollama's own
|
|
186
|
+
* runtime numbers.
|
|
187
|
+
*/
|
|
188
|
+
const ESTIMATED_KV_CACHE_MB_PER_1K_TOKENS = 32; // conservative, model-independent approximation
|
|
189
|
+
const RUNTIME_OVERHEAD_MB = 512; // Ollama server + OS scheduler slack, independent of model size
|
|
190
|
+
|
|
191
|
+
function evaluateMemoryAdmission({
|
|
192
|
+
modelName,
|
|
193
|
+
contextTokens,
|
|
194
|
+
freeBytes,
|
|
195
|
+
minFreeRamMb,
|
|
196
|
+
modelSizeGb,
|
|
197
|
+
} = {}) {
|
|
198
|
+
const sizeGb = Number.isFinite(modelSizeGb) ? modelSizeGb : (KNOWN_MODEL_SIZE_GB[modelName] ?? 8);
|
|
199
|
+
const modelMb = sizeGb * 1024;
|
|
200
|
+
const kvCacheMb = (Number(contextTokens) || 0) / 1000 * ESTIMATED_KV_CACHE_MB_PER_1K_TOKENS;
|
|
201
|
+
const requiredMb = modelMb + kvCacheMb + RUNTIME_OVERHEAD_MB + (Number(minFreeRamMb) || 0);
|
|
202
|
+
const freeMb = (Number(freeBytes) || 0) / MB;
|
|
203
|
+
const admitted = freeMb >= requiredMb;
|
|
204
|
+
return {
|
|
205
|
+
admitted,
|
|
206
|
+
freeMb: Math.round(freeMb),
|
|
207
|
+
requiredMb: Math.round(requiredMb),
|
|
208
|
+
modelEstimateMb: Math.round(modelMb),
|
|
209
|
+
kvCacheEstimateMb: Math.round(kvCacheMb),
|
|
210
|
+
reserveMb: Number(minFreeRamMb) || 0,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Full recommendation flow (PRD §22 "Memory admission algorithm"):
|
|
216
|
+
* try the profile's preferred context, shrink it, then fall back to the
|
|
217
|
+
* profile's smaller model, before ever declaring the profile unusable.
|
|
218
|
+
* Never recommends cloud — the worst outcome this function can return is
|
|
219
|
+
* `{admitted:false}` with a human-readable explanation, which callers treat
|
|
220
|
+
* as "run deterministic-only" (PRD §23.4).
|
|
221
|
+
*/
|
|
222
|
+
function recommendAdmission(opts = {}) {
|
|
223
|
+
const result = _recommendAdmissionCore(opts);
|
|
224
|
+
// Adversarial-review fix (2026-09): a memory-admission ESTIMATE that
|
|
225
|
+
// actually caused a real OOM (ollama-provider.js's callOllamaChat now
|
|
226
|
+
// records this via oom-feedback.js) used to have no way to affect a
|
|
227
|
+
// future admission decision for the SAME model on the SAME machine — the
|
|
228
|
+
// exact same "admitted: true" would repeat forever. This does not
|
|
229
|
+
// recalibrate the underlying estimate (that needs real hardware variety
|
|
230
|
+
// one machine's observed failures can't substitute for); it attaches an
|
|
231
|
+
// honest warning so the decision is no longer presented with unqualified
|
|
232
|
+
// confidence.
|
|
233
|
+
const chosenModel = result.model || opts.requestedModel;
|
|
234
|
+
const prior = chosenModel ? (0,_oom_feedback_js__WEBPACK_IMPORTED_MODULE_1__/* .priorOOMFor */ .NL)(chosenModel) : null;
|
|
235
|
+
if (prior) {
|
|
236
|
+
return {
|
|
237
|
+
...result,
|
|
238
|
+
priorOOMWarning: `'${chosenModel}' has previously failed with an out-of-memory error on this machine ` +
|
|
239
|
+
`(${prior.count} time${prior.count === 1 ? '' : 's'}, most recently ${new Date(prior.lastAt).toISOString()}). ` +
|
|
240
|
+
'The memory estimate below may be optimistic for your hardware.',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _recommendAdmissionCore({ profile, freeBytes, requestedContextTokens, requestedModel } = {}) {
|
|
247
|
+
const p = MEMORY_PROFILES[profile];
|
|
248
|
+
if (!p) return { admitted: false, reason: `unknown memory profile '${profile}'` };
|
|
249
|
+
|
|
250
|
+
const model = requestedModel || p.preferredModel;
|
|
251
|
+
const attempts = [];
|
|
252
|
+
|
|
253
|
+
// 1. Requested (or target) context at the requested/preferred model.
|
|
254
|
+
const primaryContext = Number.isFinite(requestedContextTokens) ? requestedContextTokens : p.targetContextTokens;
|
|
255
|
+
let check = evaluateMemoryAdmission({ modelName: model, contextTokens: primaryContext, freeBytes, minFreeRamMb: p.minFreeRamMb });
|
|
256
|
+
attempts.push({ model, contextTokens: primaryContext, ...check });
|
|
257
|
+
if (check.admitted) return { admitted: true, model, contextTokens: primaryContext, attempts };
|
|
258
|
+
|
|
259
|
+
// 2. Reduce context to the profile's conservative initial value first —
|
|
260
|
+
// PRD FR-2104: "shrink context before declaring an otherwise compatible
|
|
261
|
+
// model unusable."
|
|
262
|
+
if (primaryContext !== p.initialContextTokens) {
|
|
263
|
+
check = evaluateMemoryAdmission({ modelName: model, contextTokens: p.initialContextTokens, freeBytes, minFreeRamMb: p.minFreeRamMb });
|
|
264
|
+
attempts.push({ model, contextTokens: p.initialContextTokens, ...check });
|
|
265
|
+
if (check.admitted) return { admitted: true, model, contextTokens: p.initialContextTokens, attempts, reducedContext: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 3. Fall back to the profile's smaller model at its initial context.
|
|
269
|
+
if (p.fallbackModel && p.fallbackModel !== model) {
|
|
270
|
+
check = evaluateMemoryAdmission({ modelName: p.fallbackModel, contextTokens: p.initialContextTokens, freeBytes, minFreeRamMb: p.minFreeRamMb });
|
|
271
|
+
attempts.push({ model: p.fallbackModel, contextTokens: p.initialContextTokens, ...check });
|
|
272
|
+
if (check.admitted) {
|
|
273
|
+
return {
|
|
274
|
+
admitted: true, model: p.fallbackModel, contextTokens: p.initialContextTokens, attempts,
|
|
275
|
+
reducedContext: true, fellBackToSmallerModel: true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 4. Nothing fits — deterministic-only, never cloud.
|
|
281
|
+
return {
|
|
282
|
+
admitted: false,
|
|
283
|
+
attempts,
|
|
284
|
+
reason: `No local model/context combination fit in available memory with the configured reserve. ` +
|
|
285
|
+
`Recommend deterministic-only scanning, or free memory before retrying.`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
/***/ })
|
|
291
|
+
|
|
292
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const id = 3917;
|
|
2
|
+
export const ids = [3917];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 93917:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ verifyReleasePackage: () => (/* binding */ verifyReleasePackage)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export MINIMUM_KINDS */
|
|
12
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
13
|
+
// The verifier for a signed release assurance package (E7-03). Used by `scripts/verify-release-package.mjs` (a command-line wrapper) and by `release-view`.
|
|
14
|
+
//
|
|
15
|
+
// This file imports nothing but Node built-ins. It does not use the producer's code, so a defect in the producer cannot make it agree with itself. What it
|
|
16
|
+
// checks, each reported under its own code:
|
|
17
|
+
// FORMAT known schema, supported major version, no unknown top-level keys
|
|
18
|
+
// SIGNATURE Ed25519 over the domain label plus the canonical JSON of everything but the signature, verified with the key the CALLER supplies; the key the
|
|
19
|
+
// package names must be the same key (a package cannot vouch for itself, and a signature lifted from another package does not verify)
|
|
20
|
+
// SNAPSHOT the snapshot digest matches its fields
|
|
21
|
+
// EVIDENCE every record matches the digest in its reference; every reference matches its own digest; every reference is bound to this snapshot
|
|
22
|
+
// LINEAGE every dependency is present, there is no cycle, and the stated order is the dependency order
|
|
23
|
+
// MANDATORY every kind in the verifier's own minimum, in the caller's --require list, and in the package's own list is present
|
|
24
|
+
// COMPLETE the package may not claim a complete scan over a scan health that is not complete
|
|
25
|
+
// FINDINGS each finding's proof tier and exploitability flag equal what the finding itself says
|
|
26
|
+
// ASSERTIONS every cited record is in the package, and a "supported" statement cites at least one
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const MINIMUM_KINDS = Object.freeze(['scan-health', 'finding']);
|
|
30
|
+
const SCHEMA = 'agentic-security/release-package';
|
|
31
|
+
const DOMAIN = `${SCHEMA}@1\n`;
|
|
32
|
+
const TOP_KEYS = new Set(['schema', 'schemaVersion', 'snapshot', 'evidence', 'lineageOrder', 'requiredKinds', 'findings', 'completeness', 'policy', 'assertions', 'proves', 'doesNotProve', 'issuance', 'signature']);
|
|
33
|
+
const ROLES = new Set(['finding', 'completeness', 'context', 'control', 'inventory']);
|
|
34
|
+
|
|
35
|
+
const canon = (v) => {
|
|
36
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null);
|
|
37
|
+
if (Array.isArray(v)) return `[${v.map(canon).join(',')}]`;
|
|
38
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
|
|
39
|
+
};
|
|
40
|
+
const digest = (v) => `sha256:${node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(canon(v)).digest('hex')}`;
|
|
41
|
+
const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
42
|
+
|
|
43
|
+
function verifyReleasePackage(pkg, publicKeyPem, { require: extraRequired = [] } = {}) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const fail = (code, message) => errors.push({ code, message });
|
|
46
|
+
if (!isObj(pkg)) return { ok: false, errors: [{ code: 'FORMAT', message: 'the package is not an object' }] };
|
|
47
|
+
if (pkg.schema !== SCHEMA) fail('FORMAT', `unrecognised schema ${JSON.stringify(pkg.schema)}`);
|
|
48
|
+
const major = Number(String(pkg.schemaVersion).split('.')[0]);
|
|
49
|
+
if (major !== 1) fail('FORMAT', `unsupported version ${JSON.stringify(pkg.schemaVersion)}`);
|
|
50
|
+
for (const k of Object.keys(pkg)) if (!TOP_KEYS.has(k)) fail('FORMAT', `unrecognised top-level key "${k}", which the signature does not vouch for`);
|
|
51
|
+
if (errors.length) return { ok: false, errors };
|
|
52
|
+
|
|
53
|
+
// ---- SIGNATURE
|
|
54
|
+
const sig = pkg.signature;
|
|
55
|
+
let publicKey = null;
|
|
56
|
+
if (!publicKeyPem) fail('SIGNATURE', 'no public key was supplied');
|
|
57
|
+
else { try { publicKey = node_crypto__WEBPACK_IMPORTED_MODULE_0__.createPublicKey(publicKeyPem); } catch (e) { fail('SIGNATURE', `the public key is unusable: ${e.message}`); } }
|
|
58
|
+
if (!isObj(sig) || sig.algorithm !== 'ed25519' || typeof sig.value !== 'string') fail('SIGNATURE', 'the package is unsigned or uses an unsupported algorithm');
|
|
59
|
+
else if (publicKey) {
|
|
60
|
+
const { signature, ...rest } = pkg;
|
|
61
|
+
let ok = false;
|
|
62
|
+
try { ok = node_crypto__WEBPACK_IMPORTED_MODULE_0__.verify(null, Buffer.from(DOMAIN + canon(rest), 'utf8'), publicKey, Buffer.from(sig.value, 'base64')); } catch (e) { fail('SIGNATURE', `verification error: ${e.message}`); }
|
|
63
|
+
if (!ok && !errors.some((e) => e.code === 'SIGNATURE')) fail('SIGNATURE', 'the signature does not match the package contents: something changed after signing, or it was signed by another key');
|
|
64
|
+
const fp = node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(publicKey.export({ type: 'spki', format: 'der' })).digest('hex');
|
|
65
|
+
const named = pkg.issuance && pkg.issuance.issuer && pkg.issuance.issuer.keyFingerprint;
|
|
66
|
+
if (named !== fp) fail('SIGNATURE', 'the key the package names is not the key supplied for verification');
|
|
67
|
+
if (!pkg.issuance || pkg.issuance.independentlyCertified !== false) fail('SIGNATURE', 'a self-issued package may not claim independent certification');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- SNAPSHOT
|
|
71
|
+
const snap = pkg.snapshot;
|
|
72
|
+
if (!isObj(snap)) fail('SNAPSHOT', 'there is no snapshot');
|
|
73
|
+
else {
|
|
74
|
+
const { digest: d, immutable, ...body } = snap;
|
|
75
|
+
if (d !== digest(body)) fail('SNAPSHOT', 'the snapshot does not match its digest');
|
|
76
|
+
if (immutable !== (!!body.commit && body.dirty === false)) fail('SNAPSHOT', 'the snapshot\'s immutability flag is not what its fields imply');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- EVIDENCE
|
|
80
|
+
const entries = Array.isArray(pkg.evidence) ? pkg.evidence : [];
|
|
81
|
+
if (!Array.isArray(pkg.evidence)) fail('EVIDENCE', 'evidence is not a list');
|
|
82
|
+
const byDigest = new Map();
|
|
83
|
+
for (const [i, e] of entries.entries()) {
|
|
84
|
+
if (!isObj(e) || !isObj(e.ref) || !('payload' in e)) { fail('EVIDENCE', `entry ${i} is malformed`); continue; }
|
|
85
|
+
if (!ROLES.has(e.role)) fail('EVIDENCE', `entry ${i} has an unknown role ${JSON.stringify(e.role)}`);
|
|
86
|
+
const { digest: rd, findingProvenanceDigest, otherProvenanceDigest, ...rb } = e.ref;
|
|
87
|
+
if (rd !== digest(rb)) fail('EVIDENCE', `the reference of entry ${i} (${e.ref.kind}) does not match its own digest`);
|
|
88
|
+
if (e.ref.payloadDigest !== digest(e.payload)) fail('EVIDENCE', `the ${e.ref.kind} record in entry ${i} does not match the digest in its reference`);
|
|
89
|
+
if (isObj(snap) && e.ref.snapshotDigest !== snap.digest) fail('EVIDENCE', `the ${e.ref.kind} record in entry ${i} describes a different snapshot`);
|
|
90
|
+
if (byDigest.has(e.ref.digest)) fail('EVIDENCE', `entry ${i} duplicates another reference`);
|
|
91
|
+
byDigest.set(e.ref.digest, e);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- LINEAGE
|
|
95
|
+
for (const e of byDigest.values()) for (const dep of e.ref.restsOn || []) if (!byDigest.has(dep)) fail('LINEAGE', `${e.ref.kind} rests on a record that is not in the package (${String(dep).slice(0, 19)}...)`);
|
|
96
|
+
const state = new Map(); const order = [];
|
|
97
|
+
const visit = (d, trail) => {
|
|
98
|
+
if (state.get(d) === 2) return; if (state.get(d) === 1) { fail('LINEAGE', `cycle through ${trail.concat(d).map((x) => x.slice(7, 15)).join(' -> ')}`); return; }
|
|
99
|
+
state.set(d, 1);
|
|
100
|
+
for (const dep of (byDigest.get(d) || { ref: {} }).ref.restsOn || []) if (byDigest.has(dep)) visit(dep, trail.concat(d));
|
|
101
|
+
state.set(d, 2); order.push(d);
|
|
102
|
+
};
|
|
103
|
+
for (const d of [...byDigest.keys()].sort()) visit(d, []);
|
|
104
|
+
if (JSON.stringify(order) !== JSON.stringify(pkg.lineageOrder)) fail('LINEAGE', 'the stated order is not the dependency order');
|
|
105
|
+
if (JSON.stringify(entries.map((e) => e && e.ref && e.ref.digest)) !== JSON.stringify(pkg.lineageOrder)) fail('LINEAGE', 'the evidence list is not in the stated order');
|
|
106
|
+
|
|
107
|
+
// ---- MANDATORY
|
|
108
|
+
const kinds = new Set([...byDigest.values()].map((e) => e.ref.kind));
|
|
109
|
+
const needed = new Set([...MINIMUM_KINDS, ...extraRequired, ...(Array.isArray(pkg.requiredKinds) ? pkg.requiredKinds : [])]);
|
|
110
|
+
for (const k of MINIMUM_KINDS) if (!(Array.isArray(pkg.requiredKinds) && pkg.requiredKinds.includes(k))) fail('MANDATORY', `the package does not declare "${k}" as required`);
|
|
111
|
+
for (const k of [...needed].sort()) if (!kinds.has(k)) fail('MANDATORY', `required evidence is missing: ${k}`);
|
|
112
|
+
|
|
113
|
+
// ---- COMPLETE
|
|
114
|
+
const healthEntry = [...byDigest.values()].find((e) => e.ref.kind === 'scan-health');
|
|
115
|
+
const c = pkg.completeness;
|
|
116
|
+
if (!isObj(c)) fail('COMPLETE', 'there is no completeness statement');
|
|
117
|
+
else if (healthEntry) {
|
|
118
|
+
const hs = healthEntry.payload && healthEntry.payload.status;
|
|
119
|
+
if (c.scanStatus !== hs) fail('COMPLETE', `the package states scan status ${JSON.stringify(c.scanStatus)} but the scan health says ${JSON.stringify(hs)}`);
|
|
120
|
+
if (c.verdict === 'complete' && hs !== 'complete') fail('COMPLETE', 'the package claims a complete scan over a scan health that is not complete');
|
|
121
|
+
if (c.verdict !== 'complete' && hs === 'complete') fail('COMPLETE', 'the package calls an unfinished scan complete or the reverse: the verdict disagrees with the scan health');
|
|
122
|
+
if (JSON.stringify(c.conditions) !== JSON.stringify(Array.isArray(healthEntry.payload.conditions) ? healthEntry.payload.conditions : [])) fail('COMPLETE', 'the stated conditions differ from the scan health\'s own');
|
|
123
|
+
if (c.source !== healthEntry.ref.digest) fail('COMPLETE', 'the completeness statement cites a different scan health');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- FINDINGS
|
|
127
|
+
const findingEntries = [...byDigest.values()].filter((e) => e.ref.kind === 'finding');
|
|
128
|
+
const listed = new Map((Array.isArray(pkg.findings) ? pkg.findings : []).map((f) => [f.findingRef, f]));
|
|
129
|
+
if (listed.size !== findingEntries.length) fail('FINDINGS', `the package lists ${listed.size} findings but contains ${findingEntries.length}`);
|
|
130
|
+
for (const e of findingEntries) {
|
|
131
|
+
const f = listed.get(e.ref.digest);
|
|
132
|
+
if (!f) { fail('FINDINGS', `finding ${e.ref.payloadId} is in the package but not in its findings summary`); continue; }
|
|
133
|
+
const tier = e.payload && typeof e.payload.proofTier === 'string' ? e.payload.proofTier : 'unproven';
|
|
134
|
+
if (f.proofTier !== tier) fail('FINDINGS', `finding ${e.ref.payloadId}: the summary says proof tier ${JSON.stringify(f.proofTier)} but the finding says ${JSON.stringify(tier)}`);
|
|
135
|
+
if (f.exploitabilityProven !== (tier === 'execution-proven')) fail('FINDINGS', `finding ${e.ref.payloadId}: exploitability is stated as proven without an execution proof`);
|
|
136
|
+
if (f.integrity !== 'unmodified-since-signing') fail('FINDINGS', `finding ${e.ref.payloadId}: unexpected integrity statement`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- ASSERTIONS
|
|
140
|
+
for (const a of Array.isArray(pkg.assertions) ? pkg.assertions : []) {
|
|
141
|
+
for (const d of a.evidence || []) if (!byDigest.has(d)) fail('ASSERTIONS', `assertion ${a.id} cites a record that is not in the package`);
|
|
142
|
+
if (a.status === 'supported' && !(a.evidence || []).length) fail('ASSERTIONS', `assertion ${a.id} is "supported" but cites nothing`);
|
|
143
|
+
if (!['supported', 'unsupported', 'unknown'].includes(a.status)) fail('ASSERTIONS', `assertion ${a.id} has an unknown status`);
|
|
144
|
+
}
|
|
145
|
+
return { ok: errors.length === 0, errors, issuer: pkg.issuance && pkg.issuance.issuer, completeness: isObj(c) ? c.verdict : null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/***/ })
|
|
151
|
+
|
|
152
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export const id = 3918;
|
|
2
|
+
export const ids = [3918];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 63918:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ evaluatePolicy: () => (/* binding */ evaluatePolicy)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export _internals */
|
|
12
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
13
|
+
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(31421);
|
|
14
|
+
// Policy-as-code gate (FR-SDLC-9).
|
|
15
|
+
//
|
|
16
|
+
// Today's CI gate is `--fail-on <severity>`. That's coarse. Customers want
|
|
17
|
+
// to write nuanced rules: "fail if any sql-injection finding has
|
|
18
|
+
// confidence ≥ 0.8 AND the file is under src/api/", or "fail if total
|
|
19
|
+
// exploitability score across critical findings exceeds 5".
|
|
20
|
+
//
|
|
21
|
+
// We support two modes:
|
|
22
|
+
//
|
|
23
|
+
// 1. EXTERNAL OPA: if the `opa` binary is on PATH and `--policy <file.rego>`
|
|
24
|
+
// is supplied, we shell out to `opa eval -d <file> -i <findings.json>
|
|
25
|
+
// "data.<package>.deny"`. This is the right answer for customers who
|
|
26
|
+
// already use OPA elsewhere.
|
|
27
|
+
//
|
|
28
|
+
// 2. EMBEDDED MINI: when no opa binary is available, fall back to a tiny
|
|
29
|
+
// DSL that's a strict subset of rego. Rules read top-level
|
|
30
|
+
// `package`/`deny` blocks; each `deny` is a JS-evaluable expression
|
|
31
|
+
// over findings[]. This lets the v1 ship without an external binary
|
|
32
|
+
// dep while documenting the upgrade path.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// ─── External OPA ─────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function _haveOpa() {
|
|
40
|
+
try {
|
|
41
|
+
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_1__.spawnSync)('opa', ['version'], { stdio: 'ignore', timeout: 3000 });
|
|
42
|
+
return r.status === 0;
|
|
43
|
+
} catch { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _runOpa(policyFile, findingsJsonPath, packageName) {
|
|
47
|
+
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_1__.spawnSync)('opa', [
|
|
48
|
+
'eval', '-d', policyFile, '-i', findingsJsonPath,
|
|
49
|
+
`data.${packageName}.deny`,
|
|
50
|
+
], { encoding: 'utf8', timeout: 10_000 });
|
|
51
|
+
if (r.error) return { ok: false, reason: `opa-error:${r.error.code || r.error.message}` };
|
|
52
|
+
if (r.status !== 0) return { ok: false, reason: `opa-exit:${r.status}`, stderr: r.stderr };
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(r.stdout);
|
|
55
|
+
const result = parsed.result?.[0]?.expressions?.[0]?.value;
|
|
56
|
+
return { ok: true, denials: Array.isArray(result) ? result : [] };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { ok: false, reason: `opa-output-parse:${e.message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Embedded mini DSL ────────────────────────────────────────────────────
|
|
63
|
+
//
|
|
64
|
+
// Rego is too big to reimplement. We support a tiny shape:
|
|
65
|
+
//
|
|
66
|
+
// # POLICY: agentic-security policy-gate v1
|
|
67
|
+
// deny[msg] {
|
|
68
|
+
// finding := input.findings[_]
|
|
69
|
+
// finding.severity == "critical"
|
|
70
|
+
// msg := sprintf("critical finding: %v at %v", [finding.vuln, finding.file])
|
|
71
|
+
// }
|
|
72
|
+
//
|
|
73
|
+
// Parser strategy: extract each `deny[msg] { ... }` block; translate the
|
|
74
|
+
// body to a JS predicate. The grammar we accept is:
|
|
75
|
+
//
|
|
76
|
+
// - `<lhs> == <value>` / `<lhs> != <value>` / `<lhs> > <num>` / `<lhs> < <num>`
|
|
77
|
+
// - `<lhs>` references `finding.<field>` or `input.<field>`
|
|
78
|
+
// - `msg := "..."` or `msg := sprintf("...", [args])` — the msg literal
|
|
79
|
+
// - newlines + `;` as separators
|
|
80
|
+
//
|
|
81
|
+
// Anything more complex requires the external OPA binary.
|
|
82
|
+
|
|
83
|
+
function _parseEmbedded(policyText) {
|
|
84
|
+
const blocks = [];
|
|
85
|
+
// Match each `deny[NAME] { ... }` block (or `deny { ... }`).
|
|
86
|
+
const blockRe = /\bdeny(?:\s*\[\s*(\w+)\s*\])?\s*\{([\s\S]*?)\}/g;
|
|
87
|
+
let m;
|
|
88
|
+
while ((m = blockRe.exec(policyText))) {
|
|
89
|
+
const body = m[2];
|
|
90
|
+
const conditions = [];
|
|
91
|
+
let msgExpr = `"policy violation"`;
|
|
92
|
+
for (const line of body.split(/[\n;]/)) {
|
|
93
|
+
const ln = line.trim();
|
|
94
|
+
if (!ln || ln.startsWith('#')) continue;
|
|
95
|
+
// Assignment: `<id> := <expr>`
|
|
96
|
+
const asn = ln.match(/^(\w+)\s*:=\s*(.+)$/);
|
|
97
|
+
if (asn && asn[1] !== 'msg') continue; // skip non-msg assignments
|
|
98
|
+
if (asn && asn[1] === 'msg') { msgExpr = asn[2].trim(); continue; }
|
|
99
|
+
// Comparison: `finding.<field> <op> <value>`
|
|
100
|
+
const cmp = ln.match(/^(finding|input)\.([a-zA-Z_][\w.]*)\s*(==|!=|<=|>=|<|>)\s*(.+)$/);
|
|
101
|
+
if (!cmp) continue;
|
|
102
|
+
const [, scope, field, op, valueRaw] = cmp;
|
|
103
|
+
let value = valueRaw.trim();
|
|
104
|
+
if (/^".*"$/.test(value)) value = JSON.stringify(value.slice(1, -1));
|
|
105
|
+
conditions.push({ scope, field, op, value });
|
|
106
|
+
}
|
|
107
|
+
blocks.push({ conditions, msgExpr });
|
|
108
|
+
}
|
|
109
|
+
return blocks;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _evalBlock(block, finding) {
|
|
113
|
+
for (const c of block.conditions) {
|
|
114
|
+
const lhs = _resolvePath(finding, c.field);
|
|
115
|
+
let rhs;
|
|
116
|
+
try { rhs = JSON.parse(c.value); }
|
|
117
|
+
catch { rhs = c.value.replace(/^"|"$/g, ''); }
|
|
118
|
+
if (c.op === '==' && lhs !== rhs) return null;
|
|
119
|
+
if (c.op === '!=' && lhs === rhs) return null;
|
|
120
|
+
if (c.op === '>' && !(Number(lhs) > Number(rhs))) return null;
|
|
121
|
+
if (c.op === '<' && !(Number(lhs) < Number(rhs))) return null;
|
|
122
|
+
if (c.op === '>=' && !(Number(lhs) >= Number(rhs))) return null;
|
|
123
|
+
if (c.op === '<=' && !(Number(lhs) <= Number(rhs))) return null;
|
|
124
|
+
}
|
|
125
|
+
return _renderMsg(block.msgExpr, finding);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _resolvePath(obj, dotPath) {
|
|
129
|
+
return dotPath.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _renderMsg(expr, finding) {
|
|
133
|
+
// Strip outer quotes if string literal.
|
|
134
|
+
const m = expr.match(/^["'](.+)["']$/);
|
|
135
|
+
if (m) return m[1];
|
|
136
|
+
// sprintf shape: sprintf("...", [a, b])
|
|
137
|
+
const sf = expr.match(/^sprintf\s*\(\s*"([^"]+)"\s*,\s*\[(.+)\]\s*\)$/);
|
|
138
|
+
if (sf) {
|
|
139
|
+
const fmt = sf[1];
|
|
140
|
+
const args = sf[2].split(',').map(s => _resolvePath(finding, s.trim().replace(/^finding\./, '')));
|
|
141
|
+
let i = 0;
|
|
142
|
+
return fmt.replace(/%v/g, () => String(args[i++] ?? ''));
|
|
143
|
+
}
|
|
144
|
+
return expr;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _runEmbedded(policyText, findings) {
|
|
148
|
+
const blocks = _parseEmbedded(policyText);
|
|
149
|
+
const denials = [];
|
|
150
|
+
for (const f of findings) {
|
|
151
|
+
for (const b of blocks) {
|
|
152
|
+
const msg = _evalBlock(b, f);
|
|
153
|
+
if (msg) denials.push(msg);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, denials };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Evaluate a policy file against the findings list.
|
|
163
|
+
* Returns { ok, denials, runner } — denials is an array of human-readable
|
|
164
|
+
* strings (one per violation). When denials.length > 0, the gate fails.
|
|
165
|
+
*/
|
|
166
|
+
function evaluatePolicy(policyPath, findings, opts = {}) {
|
|
167
|
+
if (!policyPath || !node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(policyPath)) {
|
|
168
|
+
return { ok: false, reason: 'policy-file-missing' };
|
|
169
|
+
}
|
|
170
|
+
const policyText = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(policyPath, 'utf8');
|
|
171
|
+
const useExternal = !opts.embeddedOnly && _haveOpa();
|
|
172
|
+
if (useExternal) {
|
|
173
|
+
// Write findings to a temp file the opa binary reads.
|
|
174
|
+
const tmp = `/tmp/as-policy-${Date.now()}.json`;
|
|
175
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(tmp, JSON.stringify({ findings }));
|
|
176
|
+
const pkgMatch = policyText.match(/^\s*package\s+([\w.]+)/m);
|
|
177
|
+
const pkg = pkgMatch ? pkgMatch[1] : 'main';
|
|
178
|
+
const r = _runOpa(policyPath, tmp, pkg);
|
|
179
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(tmp); } catch {}
|
|
180
|
+
if (r.ok) return { ...r, runner: 'opa' };
|
|
181
|
+
// Fall through to embedded on opa error.
|
|
182
|
+
}
|
|
183
|
+
const r = _runEmbedded(policyText, findings);
|
|
184
|
+
return { ...r, runner: 'embedded' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const _internals = { _parseEmbedded, _evalBlock, _runEmbedded };
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
/***/ })
|
|
191
|
+
|
|
192
|
+
};
|