@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,666 @@
|
|
|
1
|
+
export const id = 6312;
|
|
2
|
+
export const ids = [6312];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 36312:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ adapterFor: () => (/* binding */ adapterFor)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony exports NATIVE_JSONL_RECORD_KEYS, parseNativeJsonlObservations, parseOtelJsonObservations, parseEnvoyAccessLogObservations, parseK8sAuditLogObservations, parseHttpAccessLogObservations */
|
|
12
|
+
// observation-adapters.js — the RuntimeObservation ADAPTER interface, plus
|
|
13
|
+
// the one shipped implementation (`native-jsonl`) (M5 deliverable #7,
|
|
14
|
+
// "Runtime-Corroborated Digital Twin" — the runtime-observed half only,
|
|
15
|
+
// "7b"; "7a", config-declared edges, is out of scope for this whole
|
|
16
|
+
// sub-project — see the scoping doc's §4.0). FR-505, AC-29. Pure by
|
|
17
|
+
// contract: zero imports (mirrors `runtime-observation.js`'s/
|
|
18
|
+
// `observation-correlation.js`'s/`flow-grade.js`'s own precedent, one step
|
|
19
|
+
// stricter even than `observation-correlation.js`'s single-file import
|
|
20
|
+
// list — see AD/6a), no fs, no I/O, never throws.
|
|
21
|
+
//
|
|
22
|
+
// ── Why only ONE adapter ships here ─────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// FR-505 itself names an open-ended adapter list: "beginning with approved
|
|
25
|
+
// OpenTelemetry traces/service graphs, gateway/mesh metadata, cloud flow
|
|
26
|
+
// metadata, and application-provided schema-safe events." The scoping
|
|
27
|
+
// doc's §4.3 ruling is to ship the INTERFACE plus a native, operator-
|
|
28
|
+
// authored JSONL adapter first, and defer OTLP to a second increment —
|
|
29
|
+
// not because OTLP is unimportant, but because an OTLP `resourceSpans`
|
|
30
|
+
// document carries an unbounded attribute surface (`http.url` with a live
|
|
31
|
+
// query string, `db.statement`, `messaging.message.payload`, arbitrary
|
|
32
|
+
// `gen_ai.*` prompt/completion attributes under active, ongoing extension
|
|
33
|
+
// by the OpenTelemetry semantic-conventions project). Mapping that surface
|
|
34
|
+
// safely means writing and defending an allowlist against an adversarial,
|
|
35
|
+
// EVOLVING external vocabulary — real, separately-scoped work with its
|
|
36
|
+
// own review, not something to bundle into the increment that also
|
|
37
|
+
// invents the RuntimeObservation contract (Task 1), the correlation match
|
|
38
|
+
// ladder (Task 2), and the import-keyed store (Task 4). This module exists
|
|
39
|
+
// so THAT increment is "write a second implementation of an
|
|
40
|
+
// already-proven interface," not "invent the interface and the mapping at
|
|
41
|
+
// the same time."
|
|
42
|
+
//
|
|
43
|
+
// ── Why external evidence arrives as a FILE, never live ingestion ──────
|
|
44
|
+
//
|
|
45
|
+
// FR-505's own no-egress rule, the root `CLAUDE.md`'s "no runtime cloud
|
|
46
|
+
// calls" convention, and `posture/runtime-correlation.js`'s own already-
|
|
47
|
+
// proven offline-file contract (an operator-supplied eBPF trace JSONL,
|
|
48
|
+
// read from disk, never pulled from a live collector by this codebase)
|
|
49
|
+
// all point the same direction: this module takes a TEXT STRING an
|
|
50
|
+
// operator has already exported and already has on disk, and returns
|
|
51
|
+
// drafts — it has no fetch, no socket, no fs call of any kind (AD/6a pins
|
|
52
|
+
// this structurally, not just by convention). The CLI that will eventually
|
|
53
|
+
// call this (a future task) is what reads the file; this module never
|
|
54
|
+
// does.
|
|
55
|
+
//
|
|
56
|
+
// ── Why the wire key set is CLOSED, exactly like the record it feeds ───
|
|
57
|
+
//
|
|
58
|
+
// `runtime-observation.js`'s own header explains at length why a
|
|
59
|
+
// RuntimeObservation is closed-world rather than open-world: it is built
|
|
60
|
+
// from operator-supplied telemetry this codebase never generated and
|
|
61
|
+
// cannot vouch for, and PRD line 983's own "reject fields capable of
|
|
62
|
+
// carrying payload values" requirement is a REJECT requirement, not a
|
|
63
|
+
// redact-and-hope one. That reasoning applies one layer further out, at
|
|
64
|
+
// the WIRE itself: `NATIVE_JSONL_RECORD_KEYS` is a closed allowlist of the
|
|
65
|
+
// only top-level JSON keys a native-JSONL record may carry, and any other
|
|
66
|
+
// top-level key — an attacker's `prompt`, an operator's well-meaning
|
|
67
|
+
// `note`, or (the smuggling attempt nobody would think to test on their
|
|
68
|
+
// own) a pre-declared `matchedNodeIds`/`matchConfidence` — is rejected,
|
|
69
|
+
// never ignored (AD/4c). `id`/`matchedNodeIds`/`matchedEdgeIds`/
|
|
70
|
+
// `matchedFlowIds`/`matchMethod`/`matchConfidence` are refused from the
|
|
71
|
+
// wire for a reason specific to THIS module: those six fields are
|
|
72
|
+
// minted/derived downstream (an id at import time, a match at correlation
|
|
73
|
+
// time) — if the wire could supply them directly, a compromised exporter,
|
|
74
|
+
// or an operator simply copy-pasting an already-matched record from one
|
|
75
|
+
// import into a hand-edited new one, could dictate what the graph BELIEVES
|
|
76
|
+
// was observed, bypassing the match ladder (`observation-correlation.js`,
|
|
77
|
+
// Task 2) entirely. None of the six are members of `NATIVE_JSONL_RECORD_KEYS`,
|
|
78
|
+
// so the SAME closed-world sweep that rejects an unrecognized field also
|
|
79
|
+
// rejects every one of these — there is no separate check to keep in sync
|
|
80
|
+
// (AD/4b, error 4).
|
|
81
|
+
//
|
|
82
|
+
// ── The two-layer rejection split (this module vs. `validateRuntimeObservation`) ──
|
|
83
|
+
//
|
|
84
|
+
// This module enforces the WIRE SHAPE only: `attributes` must be a plain
|
|
85
|
+
// object whose every value is a scalar (string/number/boolean) or an
|
|
86
|
+
// array of strings, no top-level key outside `NATIVE_JSONL_RECORD_KEYS`,
|
|
87
|
+
// and `eventCountBand`/`firstObservedAt`/`lastObservedAt` must each be a
|
|
88
|
+
// non-empty string. It does NOT validate WHICH attribute keys are
|
|
89
|
+
// approved — `validateRuntimeObservation` (Task 1, `runtime-observation.js`)
|
|
90
|
+
// is the single authority on that allowlist, and this module never
|
|
91
|
+
// duplicates it (a duplicated allowlist is exactly the kind of thing that
|
|
92
|
+
// silently drifts). Against the payload fixture, this means the adapter
|
|
93
|
+
// itself catches only 2 of the fixture's 4 smuggling attempts (line 3's
|
|
94
|
+
// top-level `prompt`, line 4's `matchedNodeIds`/`matchConfidence`) — the
|
|
95
|
+
// other two (line 1's `http.url` attribute key, line 2's `db.statement`
|
|
96
|
+
// attribute key) are structurally fine at the wire layer and are caught
|
|
97
|
+
// one layer up, by `validateRuntimeObservation`, at IMPORT time
|
|
98
|
+
// (`CLI/import-4`, a future task's own test — not a hole in this one).
|
|
99
|
+
// Any adapter error means the whole file yields nothing usable: this
|
|
100
|
+
// module always returns whatever drafts it produced regardless of
|
|
101
|
+
// `errors.length`, but the IMPORT command (a future task) refuses the
|
|
102
|
+
// file WHOLE the moment `errors.length > 0` — that refuse-the-whole-import
|
|
103
|
+
// behavior is that future task's own to prove, not this module's.
|
|
104
|
+
//
|
|
105
|
+
// ── The native-JSONL wire format, worked example ────────────────────────
|
|
106
|
+
//
|
|
107
|
+
// One JSON object per line (no comments — JSONL admits none), each with
|
|
108
|
+
// up to 5 top-level keys (`NATIVE_JSONL_RECORD_KEYS`):
|
|
109
|
+
//
|
|
110
|
+
// {"environment":"production","attributes":{"destination.host":"api.stripe.com","destination.scheme":"https","tls.version":"1.3"},"eventCountBand":"101-1k","firstObservedAt":"2026-08-02T10:00:00.000Z","lastObservedAt":"2026-08-30T10:00:00.000Z"}
|
|
111
|
+
//
|
|
112
|
+
// `environment` is optional per line — when omitted, the record inherits
|
|
113
|
+
// the default environment of the surrounding call, `context.environment`
|
|
114
|
+
// (supplied by the CALLER, never by the file). `attributes` keys must be
|
|
115
|
+
// drawn from `RUNTIME_ATTRIBUTE_KEYS` (`runtime-observation.js`) — this
|
|
116
|
+
// module does not check that, but a key outside that list will be
|
|
117
|
+
// rejected one layer up, at import time. `eventCountBand` is a BAND
|
|
118
|
+
// (`RUNTIME_OBSERVATION_VERSION`'s sibling `EVENT_COUNT_BANDS`, e.g.
|
|
119
|
+
// `'1'`/`'2-10'`/`'11-100'`/`'101-1k'`/`'1k+'`), never a raw count.
|
|
120
|
+
// `firstObservedAt`/`lastObservedAt` are ISO-8601 date-times.
|
|
121
|
+
//
|
|
122
|
+
// ── `version` is caller-supplied, not read from `runtime-observation.js` ──
|
|
123
|
+
//
|
|
124
|
+
// This is precisely why this module has NO import at all, not even of
|
|
125
|
+
// `runtime-observation.js`'s own `RUNTIME_OBSERVATION_VERSION` constant:
|
|
126
|
+
// the CLI (a future task) passes that value in as `context.version`. A
|
|
127
|
+
// zero-import module can never itself drift from the contract it feeds —
|
|
128
|
+
// there is no cached/stale constant here to go stale.
|
|
129
|
+
|
|
130
|
+
// The 5 allowed top-level keys of a native-JSONL wire record. Any other
|
|
131
|
+
// top-level key — including every one of the six RuntimeObservation
|
|
132
|
+
// fields that are minted/derived downstream (`id`, `matchedNodeIds`,
|
|
133
|
+
// `matchedEdgeIds`, `matchedFlowIds`, `matchMethod`, `matchConfidence`) —
|
|
134
|
+
// is rejected by the closed-world sweep below, never ignored.
|
|
135
|
+
const NATIVE_JSONL_RECORD_KEYS = Object.freeze([
|
|
136
|
+
'environment', 'attributes', 'eventCountBand', 'firstObservedAt', 'lastObservedAt',
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
function _isPlainObject(v) {
|
|
140
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _isNonEmptyString(v) {
|
|
144
|
+
return typeof v === 'string' && v.length > 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _isScalar(v) {
|
|
148
|
+
const t = typeof v;
|
|
149
|
+
return t === 'string' || t === 'number' || t === 'boolean';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The WIRE shape only — never which attribute keys are approved. A value
|
|
153
|
+
// must be a scalar, or an array of strings (mirroring
|
|
154
|
+
// `RUNTIME_ARRAY_ATTRIBUTE_KEYS`'s own shape one layer up, without this
|
|
155
|
+
// module knowing which specific key that applies to).
|
|
156
|
+
function _attributesShapeOk(attrs) {
|
|
157
|
+
if (!_isPlainObject(attrs)) return false;
|
|
158
|
+
for (const value of Object.values(attrs)) {
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
if (!value.every((x) => typeof x === 'string')) return false;
|
|
161
|
+
} else if (!_isScalar(value)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parses native-JSONL runtime-observation text into RuntimeObservation
|
|
170
|
+
* DRAFTS — every field a RuntimeObservation record needs EXCEPT `id`,
|
|
171
|
+
* `matchedNodeIds`, `matchedEdgeIds`, `matchedFlowIds`, `matchMethod`,
|
|
172
|
+
* `matchConfidence`, which are minted/derived downstream and can never be
|
|
173
|
+
* supplied by the wire. `context` is
|
|
174
|
+
* `{version, adapter, source, environment, windowStart, windowEnd, importedAt, retention}`
|
|
175
|
+
* — every draft carries these verbatim, EXCEPT `environment`, which a
|
|
176
|
+
* per-line `environment` key overrides.
|
|
177
|
+
*
|
|
178
|
+
* Never throws. `text` must be a string and `context` a plain object, or
|
|
179
|
+
* this returns `{drafts: [], errors: [{line: 0, message: '...'}]}`
|
|
180
|
+
* immediately. Blank lines (and the trailing newline JSONL files
|
|
181
|
+
* ordinarily end with) are skipped silently — never reported as errors.
|
|
182
|
+
* A malformed line (invalid JSON, wrong top-level shape, an unapproved
|
|
183
|
+
* top-level key, a malformed `attributes`/`eventCountBand`/
|
|
184
|
+
* `firstObservedAt`/`lastObservedAt`) yields exactly ONE `{line, message}`
|
|
185
|
+
* error for that line (`line` 1-based) and is skipped — every OTHER line
|
|
186
|
+
* still parses, so an operator sees every problem in one pass rather than
|
|
187
|
+
* stopping at the first.
|
|
188
|
+
*/
|
|
189
|
+
function parseNativeJsonlObservations(text, context) {
|
|
190
|
+
if (typeof text !== 'string' || !_isPlainObject(context)) {
|
|
191
|
+
return {
|
|
192
|
+
drafts: [],
|
|
193
|
+
errors: [{ line: 0, message: 'parseNativeJsonlObservations requires a string text and a plain-object context' }],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const {
|
|
198
|
+
version, adapter, source, environment: defaultEnvironment,
|
|
199
|
+
windowStart, windowEnd, importedAt, retention,
|
|
200
|
+
} = context;
|
|
201
|
+
|
|
202
|
+
const drafts = [];
|
|
203
|
+
const errors = [];
|
|
204
|
+
|
|
205
|
+
const lines = text.split(/\r?\n/);
|
|
206
|
+
for (let i = 0; i < lines.length; i++) {
|
|
207
|
+
const rawLine = lines[i];
|
|
208
|
+
if (rawLine.trim().length === 0) continue; // blank line, or the trailing newline's own empty tail
|
|
209
|
+
const line = i + 1;
|
|
210
|
+
|
|
211
|
+
let record;
|
|
212
|
+
try {
|
|
213
|
+
record = JSON.parse(rawLine);
|
|
214
|
+
} catch {
|
|
215
|
+
// M2 (final review): every OTHER rejection path in this module is
|
|
216
|
+
// key-only/value-free by design — V8's own JSON.parse error message
|
|
217
|
+
// quotes a snippet of the offending input (`e.message`), which would
|
|
218
|
+
// make this the sole value-echo channel in the whole command. Name
|
|
219
|
+
// only the line number, never the parse error's own text.
|
|
220
|
+
errors.push({ line, message: `invalid JSON on line ${line}` });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!_isPlainObject(record)) {
|
|
225
|
+
const gotType = record === null ? 'null' : Array.isArray(record) ? 'array' : typeof record;
|
|
226
|
+
errors.push({ line, message: `record must be a JSON object, got ${gotType}` });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const unknownKeys = Object.keys(record).filter((k) => !NATIVE_JSONL_RECORD_KEYS.includes(k));
|
|
231
|
+
if (unknownKeys.length > 0) {
|
|
232
|
+
errors.push({
|
|
233
|
+
line,
|
|
234
|
+
message: `unknown field(s) not permitted on the wire: ${unknownKeys.join(', ')} — native-JSONL records are closed-world (mirroring RuntimeObservation's own closed-world rule): only ${NATIVE_JSONL_RECORD_KEYS.join('/')} are accepted, and this rejects every one of id/matchedNodeIds/matchedEdgeIds/matchedFlowIds/matchMethod/matchConfidence too, since none are members of that allowlist`,
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!_attributesShapeOk(record.attributes)) {
|
|
240
|
+
errors.push({
|
|
241
|
+
line,
|
|
242
|
+
message: 'attributes must be a plain object whose every value is a scalar (string/number/boolean) or an array of strings',
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!_isNonEmptyString(record.eventCountBand)
|
|
248
|
+
|| !_isNonEmptyString(record.firstObservedAt)
|
|
249
|
+
|| !_isNonEmptyString(record.lastObservedAt)) {
|
|
250
|
+
errors.push({
|
|
251
|
+
line,
|
|
252
|
+
message: 'eventCountBand, firstObservedAt, and lastObservedAt must each be present and a non-empty string',
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
drafts.push({
|
|
258
|
+
version,
|
|
259
|
+
adapter,
|
|
260
|
+
source,
|
|
261
|
+
environment: 'environment' in record ? record.environment : defaultEnvironment,
|
|
262
|
+
windowStart,
|
|
263
|
+
windowEnd,
|
|
264
|
+
attributes: record.attributes,
|
|
265
|
+
eventCountBand: record.eventCountBand,
|
|
266
|
+
firstObservedAt: record.firstObservedAt,
|
|
267
|
+
lastObservedAt: record.lastObservedAt,
|
|
268
|
+
retention,
|
|
269
|
+
importedAt,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { drafts, errors };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── The four PRD "Runtime-Corroborated Security Twin" adapters ─────────
|
|
277
|
+
//
|
|
278
|
+
// Each of the four parsers below follows the identical contract the
|
|
279
|
+
// native-jsonl adapter above already established: pure (zero imports, no
|
|
280
|
+
// fs, never throws), `(text, context) -> {drafts, errors}`, one
|
|
281
|
+
// `{line, message}` error per malformed line (never a value echo), every
|
|
282
|
+
// OTHER line still parses. Each produces drafts carrying ONLY attribute
|
|
283
|
+
// keys already on `RUNTIME_ATTRIBUTE_KEYS` (`runtime-observation.js`) —
|
|
284
|
+
// none of these parsers imports that file (this module stays zero-import
|
|
285
|
+
// by contract), so an attribute key typo here is caught one layer up, at
|
|
286
|
+
// import time, exactly like the native-jsonl adapter's own documented
|
|
287
|
+
// two-layer split. Each adapter deliberately DROPS every field of its
|
|
288
|
+
// source format with no safe, approved destination — a raw URL path/query
|
|
289
|
+
// string, a user agent, a client IP, a trace/span id, a username/identity
|
|
290
|
+
// — rather than inventing a new attribute key to carry it (PRD "Privacy
|
|
291
|
+
// Protection" P0-7: schema/field classification only, never a captured
|
|
292
|
+
// value). What each adapter keeps and drops is documented at its own
|
|
293
|
+
// definition.
|
|
294
|
+
|
|
295
|
+
const _IDENTIFIER_ATTR_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
296
|
+
|
|
297
|
+
// Only ever WRITES a value that already looks like an approved identifier
|
|
298
|
+
// — never force-sanitizes an unsafe value into a misleadingly different
|
|
299
|
+
// one. Returns `null` (meaning "drop this attribute") for anything that
|
|
300
|
+
// doesn't already look safe, rather than guessing.
|
|
301
|
+
function _safeIdentifierOrNull(v) {
|
|
302
|
+
if (typeof v !== 'string' || v.length === 0) return null;
|
|
303
|
+
return _IDENTIFIER_ATTR_RE.test(v) ? v : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// `"host:port"` -> `{host, port}` (port `null` when absent/unparseable).
|
|
307
|
+
// Never a full URL parse — this shape (Envoy's `upstream_host`, a bare
|
|
308
|
+
// `host:port` pair) has no scheme/path to confuse it with.
|
|
309
|
+
function _splitHostPort(v) {
|
|
310
|
+
if (typeof v !== 'string' || v.length === 0) return { host: null, port: null };
|
|
311
|
+
const i = v.lastIndexOf(':');
|
|
312
|
+
if (i === -1) return { host: _safeIdentifierOrNull(v), port: null };
|
|
313
|
+
const host = v.slice(0, i);
|
|
314
|
+
const portStr = v.slice(i + 1);
|
|
315
|
+
const port = /^[0-9]{1,5}$/.test(portStr) ? Number(portStr) : null;
|
|
316
|
+
return { host: _safeIdentifierOrNull(host), port };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function _draftBase(context, environmentOverride) {
|
|
320
|
+
const { version, adapter, source, environment, windowStart, windowEnd, importedAt, retention } = context;
|
|
321
|
+
return {
|
|
322
|
+
version, adapter, source,
|
|
323
|
+
environment: environmentOverride !== undefined ? environmentOverride : environment,
|
|
324
|
+
windowStart, windowEnd, retention, importedAt,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* OpenTelemetry JSON trace export (the standard OTLP/JSON `resourceSpans`
|
|
330
|
+
* document — a SINGLE JSON object, never JSONL; this is the format
|
|
331
|
+
* `otlptracehttp` with `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json`
|
|
332
|
+
* writes). One draft per span. Reads only well-known OpenTelemetry
|
|
333
|
+
* semantic-convention attribute keys — `server.address`/`net.peer.name`,
|
|
334
|
+
* `server.port`/`net.peer.port`, `url.scheme`/`http.scheme`,
|
|
335
|
+
* `network.protocol.name`, `db.system`/`db.name`,
|
|
336
|
+
* `messaging.destination.name` — mapped onto the approved
|
|
337
|
+
* `destination.*`/`network.protocol`/`schema.name` keys; `resource`-level
|
|
338
|
+
* `service.name` is carried as `service.name`. Every other span
|
|
339
|
+
* attribute (`http.url`'s full query string, `http.request.header.*`,
|
|
340
|
+
* `gen_ai.*` prompt/completion content, trace/span ids themselves) is
|
|
341
|
+
* dropped — none has a safe, approved destination. `eventCountBand` is
|
|
342
|
+
* always `'1'`: a single exported span is a single observed event, never
|
|
343
|
+
* an aggregate count OTel itself does not report.
|
|
344
|
+
*/
|
|
345
|
+
function parseOtelJsonObservations(text, context) {
|
|
346
|
+
if (typeof text !== 'string' || !_isPlainObject(context)) {
|
|
347
|
+
return { drafts: [], errors: [{ line: 0, message: 'parseOtelJsonObservations requires a string text and a plain-object context' }] };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let doc;
|
|
351
|
+
try {
|
|
352
|
+
doc = JSON.parse(text);
|
|
353
|
+
} catch {
|
|
354
|
+
return { drafts: [], errors: [{ line: 0, message: 'invalid JSON: expected one OTLP/JSON resourceSpans document' }] };
|
|
355
|
+
}
|
|
356
|
+
if (!_isPlainObject(doc) || !Array.isArray(doc.resourceSpans)) {
|
|
357
|
+
return { drafts: [], errors: [{ line: 0, message: 'expected a JSON object with a top-level "resourceSpans" array (the OTLP/JSON export shape)' }] };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const _attrMap = (attrs) => {
|
|
361
|
+
const m = {};
|
|
362
|
+
if (!Array.isArray(attrs)) return m;
|
|
363
|
+
for (const a of attrs) {
|
|
364
|
+
if (!_isPlainObject(a) || typeof a.key !== 'string' || !_isPlainObject(a.value)) continue;
|
|
365
|
+
const v = a.value;
|
|
366
|
+
if (typeof v.stringValue === 'string') m[a.key] = v.stringValue;
|
|
367
|
+
else if (typeof v.intValue === 'number' || typeof v.intValue === 'string') m[a.key] = String(v.intValue);
|
|
368
|
+
else if (typeof v.boolValue === 'boolean') m[a.key] = v.boolValue;
|
|
369
|
+
}
|
|
370
|
+
return m;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Nanoseconds-since-epoch (OTLP's own wire unit) as a string or number
|
|
374
|
+
// -> ISO-8601. Returns null (never a fabricated "now") when absent or
|
|
375
|
+
// unparseable, letting the caller fall back to the import window.
|
|
376
|
+
const _isoFromUnixNano = (v) => {
|
|
377
|
+
if (v === undefined || v === null) return null;
|
|
378
|
+
const n = typeof v === 'string' ? Number(v) : v;
|
|
379
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
380
|
+
const ms = Math.round(n / 1e6);
|
|
381
|
+
const d = new Date(ms);
|
|
382
|
+
return Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const drafts = [];
|
|
386
|
+
for (const rs of doc.resourceSpans) {
|
|
387
|
+
if (!_isPlainObject(rs)) continue;
|
|
388
|
+
const resourceAttrs = _attrMap(rs.resource?.attributes);
|
|
389
|
+
const serviceName = _safeIdentifierOrNull(resourceAttrs['service.name']);
|
|
390
|
+
|
|
391
|
+
for (const ss of Array.isArray(rs.scopeSpans) ? rs.scopeSpans : []) {
|
|
392
|
+
if (!_isPlainObject(ss)) continue;
|
|
393
|
+
for (const span of Array.isArray(ss.spans) ? ss.spans : []) {
|
|
394
|
+
if (!_isPlainObject(span)) continue;
|
|
395
|
+
const a = _attrMap(span.attributes);
|
|
396
|
+
|
|
397
|
+
const attributes = {};
|
|
398
|
+
if (serviceName) attributes['service.name'] = serviceName;
|
|
399
|
+
const host = _safeIdentifierOrNull(a['server.address'] ?? a['net.peer.name']);
|
|
400
|
+
if (host) attributes['destination.host'] = host;
|
|
401
|
+
const portRaw = a['server.port'] ?? a['net.peer.port'];
|
|
402
|
+
const port = /^[0-9]{1,5}$/.test(String(portRaw ?? '')) ? Number(portRaw) : null;
|
|
403
|
+
if (port !== null) attributes['destination.port'] = port;
|
|
404
|
+
const scheme = _safeIdentifierOrNull(a['url.scheme'] ?? a['http.scheme']);
|
|
405
|
+
if (scheme) attributes['destination.scheme'] = scheme;
|
|
406
|
+
const protocol = _safeIdentifierOrNull(a['network.protocol.name']);
|
|
407
|
+
if (protocol) attributes['network.protocol'] = protocol;
|
|
408
|
+
const storeService = _safeIdentifierOrNull(a['db.system'] ?? a['db.name'] ?? a['messaging.destination.name']);
|
|
409
|
+
if (storeService) attributes['destination.service'] = storeService;
|
|
410
|
+
|
|
411
|
+
if (Object.keys(attributes).length === 0) continue; // nothing safe to report for this span
|
|
412
|
+
|
|
413
|
+
const at = _isoFromUnixNano(span.startTimeUnixNano) ?? context.windowStart;
|
|
414
|
+
drafts.push({
|
|
415
|
+
..._draftBase(context),
|
|
416
|
+
attributes,
|
|
417
|
+
eventCountBand: '1',
|
|
418
|
+
firstObservedAt: at,
|
|
419
|
+
lastObservedAt: _isoFromUnixNano(span.endTimeUnixNano) ?? at,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { drafts, errors: [] };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Envoy's default JSON access log (`json_format` logging, one JSON
|
|
430
|
+
* object per line). Reads `upstream_host` (split into
|
|
431
|
+
* `destination.host`/`destination.port`), `upstream_cluster` (as
|
|
432
|
+
* `destination.service`), `protocol` (normalized — Envoy writes
|
|
433
|
+
* `"HTTP/1.1"`, which contains `/` and is not an approved identifier
|
|
434
|
+
* shape; lowercased and `/` stripped, e.g. `"http1.1"`), and
|
|
435
|
+
* `response_flags` (`'-'`/empty means no flag, i.e. allowed; a non-empty
|
|
436
|
+
* flags string, e.g. `"UAEX"` for unauthorized, is treated as `'denied'`
|
|
437
|
+
* via `network.securityDecision`). `start_time`/`end_time` (or
|
|
438
|
+
* `start_time` alone, with `duration` in milliseconds added) feed
|
|
439
|
+
* `firstObservedAt`/`lastObservedAt`. Everything else — client address,
|
|
440
|
+
* request path/query, user agent, request id, response code's own exact
|
|
441
|
+
* value — is dropped.
|
|
442
|
+
*/
|
|
443
|
+
function parseEnvoyAccessLogObservations(text, context) {
|
|
444
|
+
if (typeof text !== 'string' || !_isPlainObject(context)) {
|
|
445
|
+
return { drafts: [], errors: [{ line: 0, message: 'parseEnvoyAccessLogObservations requires a string text and a plain-object context' }] };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const drafts = [];
|
|
449
|
+
const errors = [];
|
|
450
|
+
const lines = text.split(/\r?\n/);
|
|
451
|
+
for (let i = 0; i < lines.length; i++) {
|
|
452
|
+
const rawLine = lines[i];
|
|
453
|
+
if (rawLine.trim().length === 0) continue;
|
|
454
|
+
const line = i + 1;
|
|
455
|
+
|
|
456
|
+
let record;
|
|
457
|
+
try {
|
|
458
|
+
record = JSON.parse(rawLine);
|
|
459
|
+
} catch {
|
|
460
|
+
errors.push({ line, message: `invalid JSON on line ${line}` });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!_isPlainObject(record)) {
|
|
464
|
+
errors.push({ line, message: `record on line ${line} must be a JSON object` });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const { host, port } = _splitHostPort(record.upstream_host);
|
|
469
|
+
const cluster = _safeIdentifierOrNull(record.upstream_cluster);
|
|
470
|
+
const protocol = typeof record.protocol === 'string'
|
|
471
|
+
? _safeIdentifierOrNull(record.protocol.toLowerCase().replace(/\//g, ''))
|
|
472
|
+
: null;
|
|
473
|
+
|
|
474
|
+
const attributes = {};
|
|
475
|
+
if (host) attributes['destination.host'] = host;
|
|
476
|
+
if (port !== null) attributes['destination.port'] = port;
|
|
477
|
+
if (cluster) attributes['destination.service'] = cluster;
|
|
478
|
+
if (protocol) attributes['network.protocol'] = protocol;
|
|
479
|
+
if (typeof record.response_flags === 'string') {
|
|
480
|
+
attributes['network.securityDecision'] = record.response_flags.trim().length === 0 || record.response_flags.trim() === '-' ? 'allowed' : 'denied';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (Object.keys(attributes).length === 0) continue; // nothing safe/present to report for this line
|
|
484
|
+
|
|
485
|
+
const startIso = typeof record.start_time === 'string' && Number.isFinite(Date.parse(record.start_time))
|
|
486
|
+
? record.start_time : context.windowStart;
|
|
487
|
+
let endIso = typeof record.end_time === 'string' && Number.isFinite(Date.parse(record.end_time))
|
|
488
|
+
? record.end_time : null;
|
|
489
|
+
if (endIso === null && typeof record.duration === 'number' && Number.isFinite(record.duration)) {
|
|
490
|
+
endIso = new Date(Date.parse(startIso) + record.duration).toISOString();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
drafts.push({
|
|
494
|
+
..._draftBase(context),
|
|
495
|
+
attributes,
|
|
496
|
+
eventCountBand: '1',
|
|
497
|
+
firstObservedAt: startIso,
|
|
498
|
+
lastObservedAt: endIso ?? startIso,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { drafts, errors };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Kubernetes `audit.k8s.io` audit-event JSONL (one event object per
|
|
507
|
+
* line — the default `webhook`/file backend format). Reads `objectRef`
|
|
508
|
+
* (`namespace` + `resource`, joined as `"<namespace>:<resource>"` into
|
|
509
|
+
* `destination.service` — never `objectRef.name`, which is frequently a
|
|
510
|
+
* user-chosen, potentially sensitive resource name) and
|
|
511
|
+
* `responseStatus.code` (mapped to `network.securityDecision`: `< 400`
|
|
512
|
+
* is `'allowed'`, `>= 400` is `'denied'` — a client error is treated as a
|
|
513
|
+
* decision here because the audit log's own admission/authorization
|
|
514
|
+
* failures surface as 401/403/409, which is exactly the "security
|
|
515
|
+
* decision" fact this adapter exists to carry). `requestReceivedTimestamp`/
|
|
516
|
+
* `stageTimestamp` feed the observed-at window. **`user.username` and
|
|
517
|
+
* every request/response body field are deliberately never read** — a
|
|
518
|
+
* K8s audit event's `user` block is exactly the "authenticated identity"
|
|
519
|
+
* PRD 2 names, and `runtime-observation.js`'s own contract has no
|
|
520
|
+
* approved field for it (a deliberate, disclosed scope boundary — see
|
|
521
|
+
* this PRD's implementation-status doc).
|
|
522
|
+
*/
|
|
523
|
+
function parseK8sAuditLogObservations(text, context) {
|
|
524
|
+
if (typeof text !== 'string' || !_isPlainObject(context)) {
|
|
525
|
+
return { drafts: [], errors: [{ line: 0, message: 'parseK8sAuditLogObservations requires a string text and a plain-object context' }] };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const drafts = [];
|
|
529
|
+
const errors = [];
|
|
530
|
+
const lines = text.split(/\r?\n/);
|
|
531
|
+
for (let i = 0; i < lines.length; i++) {
|
|
532
|
+
const rawLine = lines[i];
|
|
533
|
+
if (rawLine.trim().length === 0) continue;
|
|
534
|
+
const line = i + 1;
|
|
535
|
+
|
|
536
|
+
let record;
|
|
537
|
+
try {
|
|
538
|
+
record = JSON.parse(rawLine);
|
|
539
|
+
} catch {
|
|
540
|
+
errors.push({ line, message: `invalid JSON on line ${line}` });
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (!_isPlainObject(record)) {
|
|
544
|
+
errors.push({ line, message: `record on line ${line} must be a JSON object` });
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const objectRef = _isPlainObject(record.objectRef) ? record.objectRef : {};
|
|
549
|
+
const namespace = _safeIdentifierOrNull(objectRef.namespace);
|
|
550
|
+
const resource = _safeIdentifierOrNull(objectRef.resource);
|
|
551
|
+
const code = record.responseStatus && typeof record.responseStatus === 'object' ? record.responseStatus.code : undefined;
|
|
552
|
+
|
|
553
|
+
const attributes = {};
|
|
554
|
+
if (namespace && resource) attributes['destination.service'] = `${namespace}:${resource}`;
|
|
555
|
+
else if (resource) attributes['destination.service'] = resource;
|
|
556
|
+
if (typeof code === 'number' && Number.isFinite(code)) {
|
|
557
|
+
attributes['network.securityDecision'] = code < 400 ? 'allowed' : 'denied';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (Object.keys(attributes).length === 0) continue;
|
|
561
|
+
|
|
562
|
+
const observedAt = typeof record.requestReceivedTimestamp === 'string' && Number.isFinite(Date.parse(record.requestReceivedTimestamp))
|
|
563
|
+
? record.requestReceivedTimestamp
|
|
564
|
+
: (typeof record.stageTimestamp === 'string' && Number.isFinite(Date.parse(record.stageTimestamp)) ? record.stageTimestamp : context.windowStart);
|
|
565
|
+
|
|
566
|
+
drafts.push({
|
|
567
|
+
..._draftBase(context),
|
|
568
|
+
attributes,
|
|
569
|
+
eventCountBand: '1',
|
|
570
|
+
firstObservedAt: observedAt,
|
|
571
|
+
lastObservedAt: observedAt,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { drafts, errors };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* A generic JSON-lines HTTP access log (one JSON object per line — the
|
|
580
|
+
* shape a structured reverse proxy or load-balancer access log normalizes
|
|
581
|
+
* to; deliberately not an Apache/Nginx TEXT combined-log parser, which
|
|
582
|
+
* would need its own separate, much larger format-string handling).
|
|
583
|
+
* Reads `host`/`destination_host` -> `destination.host`,
|
|
584
|
+
* `upstream`/`upstream_service` -> `destination.service`, `scheme` ->
|
|
585
|
+
* `destination.scheme`. `timestamp` (or `time`) feeds the observed-at
|
|
586
|
+
* window. Request path/query, client IP, user agent, status code's own
|
|
587
|
+
* value, and every header are dropped.
|
|
588
|
+
*/
|
|
589
|
+
function parseHttpAccessLogObservations(text, context) {
|
|
590
|
+
if (typeof text !== 'string' || !_isPlainObject(context)) {
|
|
591
|
+
return { drafts: [], errors: [{ line: 0, message: 'parseHttpAccessLogObservations requires a string text and a plain-object context' }] };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const drafts = [];
|
|
595
|
+
const errors = [];
|
|
596
|
+
const lines = text.split(/\r?\n/);
|
|
597
|
+
for (let i = 0; i < lines.length; i++) {
|
|
598
|
+
const rawLine = lines[i];
|
|
599
|
+
if (rawLine.trim().length === 0) continue;
|
|
600
|
+
const line = i + 1;
|
|
601
|
+
|
|
602
|
+
let record;
|
|
603
|
+
try {
|
|
604
|
+
record = JSON.parse(rawLine);
|
|
605
|
+
} catch {
|
|
606
|
+
errors.push({ line, message: `invalid JSON on line ${line}` });
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (!_isPlainObject(record)) {
|
|
610
|
+
errors.push({ line, message: `record on line ${line} must be a JSON object` });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const host = _safeIdentifierOrNull(record.host ?? record.destination_host);
|
|
615
|
+
const upstream = _safeIdentifierOrNull(record.upstream ?? record.upstream_service);
|
|
616
|
+
const scheme = _safeIdentifierOrNull(record.scheme);
|
|
617
|
+
|
|
618
|
+
const attributes = {};
|
|
619
|
+
if (host) attributes['destination.host'] = host;
|
|
620
|
+
if (upstream) attributes['destination.service'] = upstream;
|
|
621
|
+
if (scheme) attributes['destination.scheme'] = scheme;
|
|
622
|
+
|
|
623
|
+
if (Object.keys(attributes).length === 0) continue;
|
|
624
|
+
|
|
625
|
+
const ts = record.timestamp ?? record.time;
|
|
626
|
+
const observedAt = typeof ts === 'string' && Number.isFinite(Date.parse(ts)) ? ts : context.windowStart;
|
|
627
|
+
|
|
628
|
+
drafts.push({
|
|
629
|
+
..._draftBase(context),
|
|
630
|
+
attributes,
|
|
631
|
+
eventCountBand: '1',
|
|
632
|
+
firstObservedAt: observedAt,
|
|
633
|
+
lastObservedAt: observedAt,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return { drafts, errors };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The adapter registry — a frozen, module-level lookup. `adapterFor`
|
|
641
|
+
// returns `null` for anything unknown, NEVER a default: an unimplemented
|
|
642
|
+
// adapter name (e.g. `'otlp'`, deferred per the scoping doc's §4.3 ruling)
|
|
643
|
+
// must be rejected outright, not silently accepted on faith that it will
|
|
644
|
+
// someday exist.
|
|
645
|
+
const _ADAPTERS = Object.freeze({
|
|
646
|
+
'native-jsonl': Object.freeze({ name: 'native-jsonl', parse: parseNativeJsonlObservations }),
|
|
647
|
+
'otel-json': Object.freeze({ name: 'otel-json', parse: parseOtelJsonObservations }),
|
|
648
|
+
'envoy-access-log': Object.freeze({ name: 'envoy-access-log', parse: parseEnvoyAccessLogObservations }),
|
|
649
|
+
'k8s-audit-log': Object.freeze({ name: 'k8s-audit-log', parse: parseK8sAuditLogObservations }),
|
|
650
|
+
'http-access-log': Object.freeze({ name: 'http-access-log', parse: parseHttpAccessLogObservations }),
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* `adapterFor(name) -> {name, parse} | null` — the registry lookup.
|
|
655
|
+
* `parse` has the `(text, context) -> {drafts, errors}` signature of
|
|
656
|
+
* `parseNativeJsonlObservations` above.
|
|
657
|
+
*/
|
|
658
|
+
function adapterFor(name) {
|
|
659
|
+
if (typeof name !== 'string') return null;
|
|
660
|
+
return Object.prototype.hasOwnProperty.call(_ADAPTERS, name) ? _ADAPTERS[name] : null;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
/***/ })
|
|
665
|
+
|
|
666
|
+
};
|