@x12i/memorix-format 1.0.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/README.md +90 -0
- package/dist/associated.d.ts +38 -0
- package/dist/associated.d.ts.map +1 -0
- package/dist/associated.js +59 -0
- package/dist/associated.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +199 -0
- package/dist/cli.js.map +1 -0
- package/dist/concept.d.ts +16 -0
- package/dist/concept.d.ts.map +1 -0
- package/dist/concept.js +75 -0
- package/dist/concept.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +49 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +4 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +51 -0
- package/dist/inspect.js.map +1 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +23 -0
- package/dist/utils.js.map +1 -0
- package/dist/validate.d.ts +14 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +211 -0
- package/dist/validate.js.map +1 -0
- package/docs/association-format.md +466 -0
- package/docs/memorix-format-additions.md +956 -0
- package/docs/memorix-format-for-dummies-association-corrections.md +113 -0
- package/docs/memorix-format.md +282 -0
- package/docs/memorix-workflow-format.md +114 -0
- package/examples/invalid-record.json +11 -0
- package/examples/valid-snapshot.json +38 -0
- package/package.json +67 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# Association format
|
|
2
|
+
|
|
3
|
+
An **association** connects one snapshot record to one or more related records. Associated content is stored as root-level properties on snapshot records using the reserved `associated` prefix.
|
|
4
|
+
|
|
5
|
+
Associations are materialized by `@x12i/memorix-associator` presets, orchestrated by `@x12i/memorix-pipeline` enrichment. See [Running associations](./running-associations.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core rule
|
|
10
|
+
|
|
11
|
+
Record-to-record association is always resolved from **snapshot record to snapshot record first**:
|
|
12
|
+
|
|
13
|
+
1. The lookup starts from the current object's snapshot record.
|
|
14
|
+
2. The matching target is another snapshot record in a `*-snapshots` collection.
|
|
15
|
+
3. Use the matched target snapshot's `recordId` to fetch non-snapshot content (`discovery`, `analysis`, `decisions`, etc.).
|
|
16
|
+
4. Copy flat `content.data` into `associated<ObjectType><ContentType>` on the source snapshot.
|
|
17
|
+
|
|
18
|
+
The snapshot resolves the link; embedded content is always fetched **after** snapshot match by `recordId` (or equivalent link field such as `concept.identifier.recordId` or `data.sourceRecordId`).
|
|
19
|
+
|
|
20
|
+
**Never join a source snapshot directly to a non-snapshot content collection by business keys** (for example `vulnerabilities-snapshots.data.assetIp` → `vulnerabilities-discovery.data.assetIp`). Same-object associations still match snapshots first, then fetch content by matched snapshot `recordId`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Naming conventions
|
|
25
|
+
|
|
26
|
+
### Legacy (live data today)
|
|
27
|
+
|
|
28
|
+
Short names without object type segment: `associatedData`, `associatedDiscovery`, `associatedAnalysis`, `associatedAssets`, `associatedSubnets`, etc.
|
|
29
|
+
|
|
30
|
+
- Remain on existing records; not bulk-migrated.
|
|
31
|
+
- Registry entries carry `"namingConvention": "legacy"`.
|
|
32
|
+
- Legacy items may include inline `recordId` on copied payloads.
|
|
33
|
+
|
|
34
|
+
### v2: `associated<ObjectType><ContentType>`
|
|
35
|
+
|
|
36
|
+
| Segment | Rule | Example |
|
|
37
|
+
|---------|------|---------|
|
|
38
|
+
| Prefix | `associated` (reserved) | — |
|
|
39
|
+
| ObjectType | PascalCase canonical object type | `security-groups` → `SecurityGroups` |
|
|
40
|
+
| ContentType | PascalCase canonical content type | `discovery` → `Discovery` |
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
associatedSecurityGroupsAnalysis
|
|
46
|
+
associatedVulnerabilitiesGroupsDiscovery
|
|
47
|
+
associatedVulnerabilitiesGroupsSnapshots
|
|
48
|
+
associatedUsersDecision
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Do **not** pluralize the content type (`Discovery`, not `Discoveries`). The property is already an array.
|
|
52
|
+
|
|
53
|
+
Registry entries for new associations carry `"namingConvention": "v2"`.
|
|
54
|
+
|
|
55
|
+
### Ranked top associations: `associatedTop<ObjectType><ContentType>`
|
|
56
|
+
|
|
57
|
+
Same copy forms as above, but candidates are **sorted by importance** then **capped**.
|
|
58
|
+
|
|
59
|
+
| Segment | Rule | Example |
|
|
60
|
+
|---------|------|---------|
|
|
61
|
+
| Prefix | `associatedTop` | — |
|
|
62
|
+
| ObjectType | PascalCase | `Vulnerabilities` |
|
|
63
|
+
| ContentType | PascalCase | `Analysis` |
|
|
64
|
+
|
|
65
|
+
Example: `associatedTopVulnerabilitiesAnalysis`
|
|
66
|
+
|
|
67
|
+
**Importance** comes from the linked object type’s descriptor `importanceIndicator` (e.g. vulnerabilities → `data.severity`), or from the association rule’s `rankBy` override.
|
|
68
|
+
|
|
69
|
+
**Default cap:** `5` (`DEFAULT_TOP_ASSOCIATED_ITEMS`). Override per rule with `maxItems`.
|
|
70
|
+
|
|
71
|
+
**Empty results:** omit the property entirely (never write `[]`).
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"associatedProperty": "associatedTopVulnerabilitiesAnalysis",
|
|
76
|
+
"maxItems": 5,
|
|
77
|
+
"rankBy": {
|
|
78
|
+
"path": "data.severity",
|
|
79
|
+
"valueKind": "severity",
|
|
80
|
+
"order": "desc"
|
|
81
|
+
},
|
|
82
|
+
"match": {
|
|
83
|
+
"collection": "vulnerabilities-snapshots",
|
|
84
|
+
"joinPaths": ["data.assetIp"]
|
|
85
|
+
},
|
|
86
|
+
"content": {
|
|
87
|
+
"collection": "vulnerabilities-analysis",
|
|
88
|
+
"linkPaths": ["data.sourceRecordId", "recordId"]
|
|
89
|
+
},
|
|
90
|
+
"payload": { "source": "target.data", "format": "v2" }
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For snapshot-mediated rules, `rankBy.path` is evaluated on the **matched snapshot** (not the copied content). Severity order: `critical > high > medium > low > none`.
|
|
95
|
+
|
|
96
|
+
### Derived (not copy-form associations)
|
|
97
|
+
|
|
98
|
+
Resolver outputs such as `associatedTopology` are **derived** fields computed during pipeline resolution — not associator copy forms. Document separately; do not rename to v2 copy naming.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Association forms
|
|
103
|
+
|
|
104
|
+
The associator supports four copy forms via `rule.payload.source` (see request schema in `@x12i/memorix-associator`).
|
|
105
|
+
|
|
106
|
+
### 1. Normal data association
|
|
107
|
+
|
|
108
|
+
**Property:** `associated<ObjectType><ContentType>`
|
|
109
|
+
|
|
110
|
+
**Copy:** linked record's `data` object (flat, not wrapped).
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"associatedSecurityGroupsAnalysis": [
|
|
115
|
+
{
|
|
116
|
+
"_system": {
|
|
117
|
+
"association": {
|
|
118
|
+
"fromProperty": "data.securityGroups",
|
|
119
|
+
"toProperty": "identifier",
|
|
120
|
+
"associatedAt": "2026-07-08T09:00:00.000Z",
|
|
121
|
+
"recordId": "security-group-record-id-1"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"summary": "The group appears to provide privileged access.",
|
|
125
|
+
"impact": "High",
|
|
126
|
+
"classification": "Privileged Security Group"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Request payload:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"payload": {
|
|
137
|
+
"format": "v2",
|
|
138
|
+
"source": "target.data",
|
|
139
|
+
"system": { "include": true, "fromProperty": "data.securityGroups", "toProperty": "identifier" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Rules:
|
|
145
|
+
|
|
146
|
+
- Business data stays flat beside `_system`.
|
|
147
|
+
- Do **not** wrap copied data inside another `data` object.
|
|
148
|
+
- `_system` is optional. Omit when consumed as business or LLM context.
|
|
149
|
+
|
|
150
|
+
### 2. Snapshot root property association
|
|
151
|
+
|
|
152
|
+
**Property:** `associated<ObjectType>Snapshots<RootProperty>`
|
|
153
|
+
|
|
154
|
+
Copies a root property from the associated **snapshot** record (not a separate content collection).
|
|
155
|
+
|
|
156
|
+
Example: `associatedVulnerabilitiesGroupsSnapshotsAnalytics` — copies `analytics` from `vulnerabilities-groups-snapshots`.
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"payload": {
|
|
161
|
+
"format": "v2",
|
|
162
|
+
"source": { "snapshotRootProperty": "analytics" },
|
|
163
|
+
"system": { "include": true }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
When `system.include` is true, the item is wrapped as `{ _system, value }`. Prefer the dedicated enrichment form below when copying the `enrichment` bucket (flat copy, no `value` wrapper).
|
|
169
|
+
|
|
170
|
+
### Analytics rollups (sibling — not association)
|
|
171
|
+
|
|
172
|
+
**Do not** use association rules to *compute* counts into the source record’s own `analytics` bucket. Associations **copy** linked payloads into `associated*`. Numeric rollups are a separate JSON rule type:
|
|
173
|
+
|
|
174
|
+
- Spec: `.operational/analytics-rollups/FORMAT.md` (`memorix.analyticsRollup.v1`)
|
|
175
|
+
- Presets: `.operational/analytics-rollups/presets/*.json`
|
|
176
|
+
- Runner: `scripts/memorix-2.0-migrate/patches/run-analytics-rollups.mjs`
|
|
177
|
+
|
|
178
|
+
Use the **same join keys** as association presets (`assetIp`, `name`↔`vulnerabilityName`, CIDR). Writing `analytics` on the target is a rollup; copying another record’s `analytics` into an `associated*SnapshotsAnalytics` array is still an association (form 2 above).
|
|
179
|
+
|
|
180
|
+
### 2b. Enrichment association
|
|
181
|
+
|
|
182
|
+
**Property:** `associated<ObjectType>Enrichment`
|
|
183
|
+
|
|
184
|
+
Copies the linked **snapshot** root `enrichment` object **flat** (same shape as content associations: fields beside optional `_system`, not wrapped in `value`).
|
|
185
|
+
|
|
186
|
+
`enrichment` is an envelope bucket on snapshots (CVSS, EPSS, KEV, compositeScore, …) — **not** a separate `*-enrichment` content-type collection.
|
|
187
|
+
|
|
188
|
+
Example: `associatedVulnerabilitiesGroupsEnrichment` on `vulnerabilities-snapshots`.
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"associatedVulnerabilitiesGroupsEnrichment": [
|
|
193
|
+
{
|
|
194
|
+
"cvss": { "baseScore": 9.8 },
|
|
195
|
+
"epss": { "epss": 0.94 },
|
|
196
|
+
"kev": { "isKnownExploited": true },
|
|
197
|
+
"compositeScore": 0.97,
|
|
198
|
+
"riskLevel": "CRITICAL"
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"associatedProperty": "associatedVulnerabilitiesGroupsEnrichment",
|
|
207
|
+
"payload": {
|
|
208
|
+
"format": "v2",
|
|
209
|
+
"source": "target.enrichment",
|
|
210
|
+
"includeRecordId": false
|
|
211
|
+
},
|
|
212
|
+
"target": {
|
|
213
|
+
"collection": "vulnerabilities-groups-snapshots",
|
|
214
|
+
"joinPaths": ["data.name"]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Rules:
|
|
220
|
+
|
|
221
|
+
- Omit the property when no linked snapshot has a non-empty `enrichment` object.
|
|
222
|
+
- Do not invent a `vulnerabilities-groups-enrichment` collection for this form.
|
|
223
|
+
- Naming helper: `buildAssociatedPropertyName({ form: "enrichment", objectType: "vulnerabilities-groups" })`.
|
|
224
|
+
|
|
225
|
+
### 3. Concepts association
|
|
226
|
+
|
|
227
|
+
**Property:** `associated<ObjectType>Concepts`
|
|
228
|
+
|
|
229
|
+
Copies `concept` from associated snapshot records (not full `data`).
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"associatedSecurityGroupsConcepts": [
|
|
234
|
+
{
|
|
235
|
+
"identifier": { "kind": "group-name", "groupName": "Domain Admins" },
|
|
236
|
+
"title": "Domain Admins"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"payload": {
|
|
245
|
+
"format": "v2",
|
|
246
|
+
"source": "target.concept",
|
|
247
|
+
"system": { "include": true }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 4. Record IDs association
|
|
253
|
+
|
|
254
|
+
**Property:** `associated<ObjectType>RecordIds` (use `RecordIds`, not `RecordsIds`)
|
|
255
|
+
|
|
256
|
+
**Value:** array of strings. Must **not** include `_system`.
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"associatedSecurityGroupsRecordIds": [
|
|
261
|
+
"security-group-record-id-1",
|
|
262
|
+
"security-group-record-id-2"
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"payload": {
|
|
270
|
+
"format": "v2",
|
|
271
|
+
"source": "target.recordId"
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## `_system.association` metadata
|
|
279
|
+
|
|
280
|
+
Reserved for technical traceability — not business data.
|
|
281
|
+
|
|
282
|
+
| Field | Meaning |
|
|
283
|
+
|-------|---------|
|
|
284
|
+
| `fromProperty` | Path on the source snapshot that supplied the join value |
|
|
285
|
+
| `toProperty` | Path on the linked snapshot used for matching |
|
|
286
|
+
| `associatedAt` | ISO date-time when the association was created or refreshed |
|
|
287
|
+
| `recordId` | Linked snapshot `recordId` (v2, when enabled) |
|
|
288
|
+
|
|
289
|
+
Included when `payload.format === "v2"` and `payload.system.include === true`. All fields are optional even when `_system` is present.
|
|
290
|
+
|
|
291
|
+
Strip `_system` when presenting associated content to humans, LLMs, or downstream analysis.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Identifier and identity matching
|
|
296
|
+
|
|
297
|
+
Prefer the linked snapshot's `identity` or `concept.identifier`:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{ "identity": "group-name:Domain Admins" }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"identifier": { "kind": "group-name", "groupName": "Domain Admins" }
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Canonical location is `concept.identifier`. Root `identifier` / `identity` are accepted when present.
|
|
310
|
+
|
|
311
|
+
Generate identity strings deterministically: join identifier values in order with `:` (e.g. `assetIp:10.100.18.64`).
|
|
312
|
+
|
|
313
|
+
Association rules declare join paths in `sourceJoinPaths` and snapshot match paths in `match.joinPaths` (or legacy `target.joinPaths` for snapshot-only rules). Content fetch rules add `content.linkPaths` that reference the matched snapshot `recordId`. v2 rules should target `concept.identifier` or generated `identity` where possible.
|
|
314
|
+
|
|
315
|
+
Associator request shape for content associations:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"sourceJoinPaths": ["data.assetIp"],
|
|
320
|
+
"match": {
|
|
321
|
+
"collection": "vulnerabilities-snapshots",
|
|
322
|
+
"joinPaths": ["data.assetIp", "concept.identifier.assetIp", "recordId"]
|
|
323
|
+
},
|
|
324
|
+
"content": {
|
|
325
|
+
"collection": "vulnerabilities-discovery",
|
|
326
|
+
"contentType": "discovery",
|
|
327
|
+
"linkPaths": ["concept.identifier.recordId", "data.sourceRecordId", "recordId"]
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Worked example: security group analysis into users
|
|
335
|
+
|
|
336
|
+
Collections:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
users-snapshots
|
|
340
|
+
security-groups-snapshots
|
|
341
|
+
security-groups-analysis
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
User snapshot source:
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"data": { "securityGroups": ["Domain Admins", "VPN Users"] }
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Security group snapshot identifier:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"recordId": "security-group-record-id-1",
|
|
357
|
+
"concept": {
|
|
358
|
+
"identifier": { "kind": "group-name", "groupName": "Domain Admins" },
|
|
359
|
+
"title": "Domain Admins"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Process:
|
|
365
|
+
|
|
366
|
+
1. Start from `users-snapshots` record.
|
|
367
|
+
2. Read join values from `data.securityGroups`.
|
|
368
|
+
3. Match against `security-groups-snapshots` via `concept.identifier` / identity.
|
|
369
|
+
4. Use matched snapshot `recordId` to fetch `security-groups-analysis` records.
|
|
370
|
+
5. Copy each analysis record's `data` flat into `associatedSecurityGroupsAnalysis`.
|
|
371
|
+
|
|
372
|
+
Result:
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"recordId": "user-record-id-1",
|
|
377
|
+
"data": {
|
|
378
|
+
"username": "ami",
|
|
379
|
+
"securityGroups": ["Domain Admins"]
|
|
380
|
+
},
|
|
381
|
+
"associatedSecurityGroupsAnalysis": [
|
|
382
|
+
{
|
|
383
|
+
"_system": {
|
|
384
|
+
"association": {
|
|
385
|
+
"fromProperty": "data.securityGroups",
|
|
386
|
+
"toProperty": "concept.identifier",
|
|
387
|
+
"associatedAt": "2026-07-08T09:00:00.000Z"
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
"summary": "The group appears to provide privileged access.",
|
|
391
|
+
"impact": "High",
|
|
392
|
+
"classification": "Privileged Security Group"
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Implementation status
|
|
401
|
+
|
|
402
|
+
| Capability | Status |
|
|
403
|
+
|------------|--------|
|
|
404
|
+
| Legacy `associatedData` / `associatedDiscovery` / … presets | **Live** — registry `namingConvention: "legacy"` |
|
|
405
|
+
| v2 property naming | **Target** — new registry entries use `associated<ObjectType><ContentType>` |
|
|
406
|
+
| v2 payload forms (`target.data`, `target.concept`, `target.recordId`, snapshot root) | **Supported** in `@x12i/memorix-associator` request schema |
|
|
407
|
+
| v2 `_system.association` metadata | **Supported** when `format: "v2"` and `system.include: true` |
|
|
408
|
+
| Registry-driven preset generation | **Live** — `generate-presets-from-registry.mjs` |
|
|
409
|
+
| Full enrichment orchestration | **Live** — `run-enrichment-pipeline.mjs` |
|
|
410
|
+
|
|
411
|
+
New associations should be added as registry entries + presets using v2 naming and payload format. Existing legacy properties continue to refresh through their current presets without rename.
|
|
412
|
+
|
|
413
|
+
### Pilot: vulnerability discovery and decisions (v2)
|
|
414
|
+
|
|
415
|
+
Snapshot-mediated flow:
|
|
416
|
+
|
|
417
|
+
1. Read source `vulnerabilities-snapshots.data.assetIp`.
|
|
418
|
+
2. Match target `vulnerabilities-snapshots` by snapshot paths (`data.assetIp`, `concept.identifier.assetIp`, `identity`, `recordId`).
|
|
419
|
+
3. Take matched target snapshot `recordId`.
|
|
420
|
+
4. Fetch `vulnerabilities-discovery` / `vulnerabilities-decisions` by record-id link (`concept.identifier.recordId`, `data.sourceRecordId`, or root `recordId`).
|
|
421
|
+
5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
|
|
422
|
+
|
|
423
|
+
| Property | Match collection | Content collection | Join key |
|
|
424
|
+
|----------|------------------|--------------------|----------|
|
|
425
|
+
| `associatedVulnerabilitiesDiscovery` | `vulnerabilities-snapshots` | `vulnerabilities-discovery` | `data.assetIp` → snapshot → `recordId` |
|
|
426
|
+
| `associatedVulnerabilitiesDecisions` | `vulnerabilities-snapshots` | `vulnerabilities-decisions` | `data.assetIp` → snapshot → `recordId` |
|
|
427
|
+
|
|
428
|
+
Target source collection: `vulnerabilities-snapshots` (`MEMORIX_EVENTS_DB`).
|
|
429
|
+
|
|
430
|
+
Refresh rule keys:
|
|
431
|
+
|
|
432
|
+
- `vulnerabilities.vulnerabilities.discovery.by-assetIp`
|
|
433
|
+
- `vulnerabilities.vulnerabilities.decisions.by-assetIp`
|
|
434
|
+
|
|
435
|
+
### Pilot: asset-side vulnerability analysis (v2)
|
|
436
|
+
|
|
437
|
+
Snapshot-mediated flow (source is **assets**, not vulnerabilities):
|
|
438
|
+
|
|
439
|
+
1. Read source `assets-snapshots.data.assetIps` (also `data.assetIp`, `data.ip_address`).
|
|
440
|
+
2. Match `vulnerabilities-snapshots` by `data.assetIp` / `concept.identifier.assetIp`.
|
|
441
|
+
3. Take matched snapshot `recordId`.
|
|
442
|
+
4. Fetch `vulnerabilities-analysis` by record-id link.
|
|
443
|
+
5. Copy flat `content.data` into `associatedVulnerabilitiesAnalysis` on the asset snapshot.
|
|
444
|
+
|
|
445
|
+
| Property | Match collection | Content collection | Join key |
|
|
446
|
+
|----------|------------------|--------------------|----------|
|
|
447
|
+
| `associatedVulnerabilitiesAnalysis` | `vulnerabilities-snapshots` | `vulnerabilities-analysis` | `data.assetIps` → snapshot `data.assetIp` → `recordId` |
|
|
448
|
+
|
|
449
|
+
Target source collection: `assets-snapshots` (`MEMORIX_ENTITIES_DB`).
|
|
450
|
+
|
|
451
|
+
Refresh rule key: `assets.vulnerabilities.analysis.by-assetIp`
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Legacy properties reference
|
|
456
|
+
|
|
457
|
+
| Property | Typical link |
|
|
458
|
+
|----------|--------------|
|
|
459
|
+
| `associatedData` | Linked snapshots / generic data |
|
|
460
|
+
| `associatedDiscovery` | Linked discovery |
|
|
461
|
+
| `associatedAnalysis` | Linked analysis |
|
|
462
|
+
| `associatedAssets` | Asset snapshots by IP |
|
|
463
|
+
| `associatedSubnets` | Subnet snapshots (explicit) |
|
|
464
|
+
| `associatedTopology` | Derived (resolver, not associator copy) |
|
|
465
|
+
| `associatedVulnerabilityGroup` | Vuln-group snapshots (explicit) |
|
|
466
|
+
| `associatedZoneAnalysis` | Topology-zone analysis |
|