@x12i/memorix-format 2.2.0 → 2.2.2
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 +69 -14
- package/docs/association-format.md +39 -39
- package/docs/memorix-format-additions.md +57 -57
- package/docs/memorix-format-for-dummies-association-corrections.md +15 -15
- package/docs/memorix-format.md +53 -103
- package/docs/memorix-workflow-format.md +1 -1
- package/examples/valid-snapshot.json +10 -10
- package/meta/inventory/managed-descriptors.json +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
# @x12i/memorix-format
|
|
2
2
|
|
|
3
|
-
Memorix **product** record format —
|
|
3
|
+
Memorix **product** record format — open-bucket conventions and migrate tooling layered on
|
|
4
4
|
[`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format).
|
|
5
5
|
|
|
6
6
|
| Package | Owns |
|
|
7
7
|
|---|---|
|
|
8
8
|
| `@x12i/memorix-base-format` | Fixed skeleton: `recordId`, `concept`, `data`, `_…`; unnamed open buckets; ContentTypeMeta |
|
|
9
|
-
| `@x12i/memorix-format` |
|
|
9
|
+
| `@x12i/memorix-format` | Known open-bucket meanings, `associated*` rules, snapshot SCOPE, built-in profiles, **migrate** CLI |
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**Object types belong in Catalox descriptors, not here.** Format docs and examples use abstract names (`items`, `groups`, `locations`, `documents`) so domain packs are never confused with format law.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Product meta lives under [`meta/`](./meta/). Managed-descriptor inventory in `meta/inventory/` is not format law.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Normative: [docs/memorix-format-requirements.md](./docs/memorix-format-requirements.md). Narrative: [docs/memorix-format.md](./docs/memorix-format.md).
|
|
16
|
+
|
|
17
|
+
## Layering
|
|
18
|
+
|
|
19
|
+
1. Base envelope (identity / core / underscore / open).
|
|
20
|
+
2. Product open buckets when present: `analytics`, `enrichment`, `insights`, `associated*`.
|
|
21
|
+
3. **SCOPE:** full open-bucket surface primarily on **snapshots**; other content types keep substance in `.data` unless ContentTypeMeta opts in.
|
|
22
|
+
4. Readers / human projection **omit** root `_…` and `concept._extIdentifier` by default; keep `recordId` for joins.
|
|
18
23
|
|
|
19
24
|
## Install
|
|
20
25
|
|
|
@@ -23,6 +28,27 @@ npm install @x12i/memorix-format
|
|
|
23
28
|
# pulls @x12i/memorix-base-format
|
|
24
29
|
```
|
|
25
30
|
|
|
31
|
+
## Abstract snapshot example
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"recordId": "rec-item-1",
|
|
36
|
+
"concept": {
|
|
37
|
+
"_extIdentifier": {
|
|
38
|
+
"kind": "sku",
|
|
39
|
+
"sku": "SKU-100",
|
|
40
|
+
"key": "sku:SKU-100"
|
|
41
|
+
},
|
|
42
|
+
"title": "Blue widget"
|
|
43
|
+
},
|
|
44
|
+
"data": { "sku": "SKU-100", "color": "blue" },
|
|
45
|
+
"associatedData": [{ "locationCode": "WH-A", "qty": 12 }],
|
|
46
|
+
"analytics": { "linkedLocations": { "total": 1 } },
|
|
47
|
+
"insights": { "priority": { "level": "high", "confidence": 0.9 } },
|
|
48
|
+
"_system": { "state": "active" }
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
26
52
|
## SDK
|
|
27
53
|
|
|
28
54
|
```ts
|
|
@@ -33,14 +59,12 @@ import {
|
|
|
33
59
|
snapshotsContentTypeMeta,
|
|
34
60
|
} from "@x12i/memorix-format";
|
|
35
61
|
|
|
36
|
-
// Product validation (base + ASSOC/SCOPE + optional profile)
|
|
37
62
|
validateMemorixRecord(record, {
|
|
38
63
|
profile: "snapshots",
|
|
39
|
-
objectType: "
|
|
64
|
+
objectType: "items",
|
|
40
65
|
contentType: "snapshots",
|
|
41
66
|
});
|
|
42
67
|
|
|
43
|
-
// Base only (no product vocabulary)
|
|
44
68
|
validateMemorixBaseRecord(record);
|
|
45
69
|
classifyRootKey("associatedData"); // "open"
|
|
46
70
|
```
|
|
@@ -48,13 +72,44 @@ classifyRootKey("associatedData"); // "open"
|
|
|
48
72
|
## CLI
|
|
49
73
|
|
|
50
74
|
```bash
|
|
51
|
-
npx memorix-format validate ./record.json --profile=snapshots --object-type=
|
|
75
|
+
npx memorix-format validate ./record.json --profile=snapshots --object-type=items
|
|
52
76
|
npx memorix-format validate ./dir/ --content-type=events
|
|
53
77
|
npx memorix-format profiles
|
|
54
78
|
npx memorix-format docs memorix-format-requirements.md
|
|
55
79
|
```
|
|
56
80
|
|
|
57
|
-
##
|
|
81
|
+
## Migrate (recommended transform)
|
|
82
|
+
|
|
83
|
+
Present-tense format only. Transform exports / sample JSON; **live Mongo apply is a separate ops checklist**.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Dry-run (stdout summary; validates after transform)
|
|
87
|
+
npx memorix-format migrate ./export-dir/ --content-type=snapshots
|
|
88
|
+
|
|
89
|
+
# Write cleaned files
|
|
90
|
+
npx memorix-format migrate ./export-dir/ --out=./clean-dir/
|
|
91
|
+
|
|
92
|
+
# Overwrite in place after validation passes
|
|
93
|
+
npx memorix-format migrate ./records.json --in-place
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Defaults applied by migrate:
|
|
97
|
+
|
|
98
|
+
- `concept.identifier` → `concept._extIdentifier` (`identifierKey` → `key`)
|
|
99
|
+
- root `identity` / `lifecycle` / `sourceFacts` / `index` removed (lifecycle events folded into `_system.lifecycle`)
|
|
100
|
+
- root `_graphRuns` / `_jobTypeRuns` → under `_system`
|
|
101
|
+
- `_system.state` set to `active` when missing (maps old `lifecycle.status` when present)
|
|
102
|
+
|
|
103
|
+
Writers set `_system.state: "active"` on new records. Pipeline format enforce defaults to **hard** (`MEMORIX_FORMAT_ENFORCE=warn` to soften during cutover).
|
|
104
|
+
|
|
105
|
+
### Runbook
|
|
106
|
+
|
|
107
|
+
1. Dry-run against an export or package examples.
|
|
108
|
+
2. Write `--out` and re-validate.
|
|
109
|
+
3. Only then apply to Mongo under ops change control (not part of this package’s default workflow).
|
|
110
|
+
|
|
111
|
+
## Related
|
|
58
112
|
|
|
59
|
-
|
|
60
|
-
|
|
113
|
+
- Base envelope SOT: [`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format)
|
|
114
|
+
- Associations: [docs/association-format.md](./docs/association-format.md)
|
|
115
|
+
- Changelog: [CHANGELOG.md](./CHANGELOG.md)
|
|
@@ -15,9 +15,9 @@ Record-to-record association is always resolved from **snapshot record to snapsh
|
|
|
15
15
|
3. Use the matched target snapshot's `recordId` to fetch non-snapshot content (`discovery`, `analysis`, `decisions`, etc.).
|
|
16
16
|
4. Copy flat `content.data` into `associated<ObjectType><ContentType>` on the source snapshot.
|
|
17
17
|
|
|
18
|
-
The snapshot resolves the link; embedded content is always fetched **after** snapshot match by `recordId` (or equivalent link field such as `concept.
|
|
18
|
+
The snapshot resolves the link; embedded content is always fetched **after** snapshot match by `recordId` (or equivalent link field such as `concept._extIdentifier.recordId` or `data.sourceRecordId`).
|
|
19
19
|
|
|
20
|
-
**Never join a source snapshot directly to a non-snapshot content collection by business keys** (for example `
|
|
20
|
+
**Never join a source snapshot directly to a non-snapshot content collection by business keys** (for example `findings-snapshots.data.sku` → `findings-discovery.data.sku`). Same-object associations still match snapshots first, then fetch content by matched snapshot `recordId`.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -64,7 +64,7 @@ Same copy forms as above, but candidates are **sorted by importance** then **cap
|
|
|
64
64
|
|
|
65
65
|
Example: `associatedTopVulnerabilitiesAnalysis`
|
|
66
66
|
|
|
67
|
-
**Importance** comes from the linked object type’s descriptor `importanceIndicator` (e.g.
|
|
67
|
+
**Importance** comes from the linked object type’s descriptor `importanceIndicator` (e.g. findings → `data.severity`), or from the association rule’s `rankBy` override.
|
|
68
68
|
|
|
69
69
|
**Default cap:** `5` (`DEFAULT_TOP_ASSOCIATED_ITEMS`). Override per rule with `maxItems`.
|
|
70
70
|
|
|
@@ -80,11 +80,11 @@ Example: `associatedTopVulnerabilitiesAnalysis`
|
|
|
80
80
|
"order": "desc"
|
|
81
81
|
},
|
|
82
82
|
"match": {
|
|
83
|
-
"collection": "
|
|
84
|
-
"joinPaths": ["data.
|
|
83
|
+
"collection": "findings-snapshots",
|
|
84
|
+
"joinPaths": ["data.sku"]
|
|
85
85
|
},
|
|
86
86
|
"content": {
|
|
87
|
-
"collection": "
|
|
87
|
+
"collection": "findings-analysis",
|
|
88
88
|
"linkPaths": ["data.sourceRecordId", "recordId"]
|
|
89
89
|
},
|
|
90
90
|
"payload": { "source": "target.data", "format": "v2" }
|
|
@@ -153,7 +153,7 @@ Rules:
|
|
|
153
153
|
|
|
154
154
|
Copies a root property from the associated **snapshot** record (not a separate content collection).
|
|
155
155
|
|
|
156
|
-
Example: `associatedVulnerabilitiesGroupsSnapshotsAnalytics` — copies `analytics` from `
|
|
156
|
+
Example: `associatedVulnerabilitiesGroupsSnapshotsAnalytics` — copies `analytics` from `item-groups-snapshots`.
|
|
157
157
|
|
|
158
158
|
```json
|
|
159
159
|
{
|
|
@@ -175,7 +175,7 @@ When `system.include` is true, the item is wrapped as `{ _system, value }`. Pref
|
|
|
175
175
|
- Presets: `.operational/analytics-rollups/presets/*.json`
|
|
176
176
|
- Runner: `scripts/memorix-2.0-migrate/patches/run-analytics-rollups.mjs`
|
|
177
177
|
|
|
178
|
-
Use the **same join keys** as association presets (`
|
|
178
|
+
Use the **same join keys** as association presets (`sku`, `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
179
|
|
|
180
180
|
### 2b. Enrichment association
|
|
181
181
|
|
|
@@ -183,9 +183,9 @@ Use the **same join keys** as association presets (`assetIp`, `name`↔`vulnerab
|
|
|
183
183
|
|
|
184
184
|
Copies the linked **snapshot** root `enrichment` object **flat** (same shape as content associations: fields beside optional `_system`, not wrapped in `value`).
|
|
185
185
|
|
|
186
|
-
`enrichment` is an envelope bucket on snapshots (
|
|
186
|
+
`enrichment` is an envelope bucket on snapshots (external feed values as received) — **not** a separate `*-enrichment` content-type collection.
|
|
187
187
|
|
|
188
|
-
Example: `associatedVulnerabilitiesGroupsEnrichment` on `
|
|
188
|
+
Example: `associatedVulnerabilitiesGroupsEnrichment` on `findings-snapshots`.
|
|
189
189
|
|
|
190
190
|
```json
|
|
191
191
|
{
|
|
@@ -210,7 +210,7 @@ Example: `associatedVulnerabilitiesGroupsEnrichment` on `vulnerabilities-snapsho
|
|
|
210
210
|
"includeRecordId": false
|
|
211
211
|
},
|
|
212
212
|
"target": {
|
|
213
|
-
"collection": "
|
|
213
|
+
"collection": "item-groups-snapshots",
|
|
214
214
|
"joinPaths": ["data.name"]
|
|
215
215
|
}
|
|
216
216
|
}
|
|
@@ -219,8 +219,8 @@ Example: `associatedVulnerabilitiesGroupsEnrichment` on `vulnerabilities-snapsho
|
|
|
219
219
|
Rules:
|
|
220
220
|
|
|
221
221
|
- Omit the property when no linked snapshot has a non-empty `enrichment` object.
|
|
222
|
-
- Do not invent a `
|
|
223
|
-
- Naming helper: `buildAssociatedPropertyName({ form: "enrichment", objectType: "
|
|
222
|
+
- Do not invent a `item-groups-enrichment` collection for this form.
|
|
223
|
+
- Naming helper: `buildAssociatedPropertyName({ form: "enrichment", objectType: "item-groups" })`.
|
|
224
224
|
|
|
225
225
|
### 3. Concepts association
|
|
226
226
|
|
|
@@ -294,7 +294,7 @@ Strip `_system` when presenting associated content to humans, LLMs, or downstrea
|
|
|
294
294
|
|
|
295
295
|
## Identifier and identity matching
|
|
296
296
|
|
|
297
|
-
Prefer the linked snapshot's `identity` or `concept.
|
|
297
|
+
Prefer the linked snapshot's `identity` or `concept._extIdentifier`:
|
|
298
298
|
|
|
299
299
|
```json
|
|
300
300
|
{ "identity": "group-name:Domain Admins" }
|
|
@@ -306,25 +306,25 @@ Prefer the linked snapshot's `identity` or `concept.identifier`:
|
|
|
306
306
|
}
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
Canonical location is `concept.
|
|
309
|
+
Canonical location is `concept._extIdentifier`. Root `identifier` / `identity` are accepted when present.
|
|
310
310
|
|
|
311
|
-
Generate identity strings deterministically: join identifier values in order with `:` (e.g. `
|
|
311
|
+
Generate identity strings deterministically: join identifier values in order with `:` (e.g. `sku:10.100.18.64`).
|
|
312
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.
|
|
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._extIdentifier` or generated `identity` where possible.
|
|
314
314
|
|
|
315
315
|
Associator request shape for content associations:
|
|
316
316
|
|
|
317
317
|
```json
|
|
318
318
|
{
|
|
319
|
-
"sourceJoinPaths": ["data.
|
|
319
|
+
"sourceJoinPaths": ["data.sku"],
|
|
320
320
|
"match": {
|
|
321
|
-
"collection": "
|
|
322
|
-
"joinPaths": ["data.
|
|
321
|
+
"collection": "findings-snapshots",
|
|
322
|
+
"joinPaths": ["data.sku", "concept._extIdentifier.sku", "recordId"]
|
|
323
323
|
},
|
|
324
324
|
"content": {
|
|
325
|
-
"collection": "
|
|
325
|
+
"collection": "findings-discovery",
|
|
326
326
|
"contentType": "discovery",
|
|
327
|
-
"linkPaths": ["concept.
|
|
327
|
+
"linkPaths": ["concept._extIdentifier.recordId", "data.sourceRecordId", "recordId"]
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
```
|
|
@@ -365,7 +365,7 @@ Process:
|
|
|
365
365
|
|
|
366
366
|
1. Start from `users-snapshots` record.
|
|
367
367
|
2. Read join values from `data.securityGroups`.
|
|
368
|
-
3. Match against `security-groups-snapshots` via `concept.
|
|
368
|
+
3. Match against `security-groups-snapshots` via `concept._extIdentifier` / identity.
|
|
369
369
|
4. Use matched snapshot `recordId` to fetch `security-groups-analysis` records.
|
|
370
370
|
5. Copy each analysis record's `data` flat into `associatedSecurityGroupsAnalysis`.
|
|
371
371
|
|
|
@@ -383,7 +383,7 @@ Result:
|
|
|
383
383
|
"_system": {
|
|
384
384
|
"association": {
|
|
385
385
|
"fromProperty": "data.securityGroups",
|
|
386
|
-
"toProperty": "concept.
|
|
386
|
+
"toProperty": "concept._extIdentifier",
|
|
387
387
|
"associatedAt": "2026-07-08T09:00:00.000Z"
|
|
388
388
|
}
|
|
389
389
|
},
|
|
@@ -414,41 +414,41 @@ New associations should be added as registry entries + presets using v2 naming a
|
|
|
414
414
|
|
|
415
415
|
Snapshot-mediated flow:
|
|
416
416
|
|
|
417
|
-
1. Read source `
|
|
418
|
-
2. Match target `
|
|
417
|
+
1. Read source `findings-snapshots.data.sku`.
|
|
418
|
+
2. Match target `findings-snapshots` by snapshot paths (`data.sku`, `concept._extIdentifier.sku`, `identity`, `recordId`).
|
|
419
419
|
3. Take matched target snapshot `recordId`.
|
|
420
|
-
4. Fetch `
|
|
420
|
+
4. Fetch `findings-discovery` / `findings-decisions` by record-id link (`concept._extIdentifier.recordId`, `data.sourceRecordId`, or root `recordId`).
|
|
421
421
|
5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
|
|
422
422
|
|
|
423
423
|
| Property | Match collection | Content collection | Join key |
|
|
424
424
|
|----------|------------------|--------------------|----------|
|
|
425
|
-
| `associatedVulnerabilitiesDiscovery` | `
|
|
426
|
-
| `associatedVulnerabilitiesDecisions` | `
|
|
425
|
+
| `associatedVulnerabilitiesDiscovery` | `findings-snapshots` | `findings-discovery` | `data.sku` → snapshot → `recordId` |
|
|
426
|
+
| `associatedVulnerabilitiesDecisions` | `findings-snapshots` | `findings-decisions` | `data.sku` → snapshot → `recordId` |
|
|
427
427
|
|
|
428
|
-
Target source collection: `
|
|
428
|
+
Target source collection: `findings-snapshots` (`MEMORIX_EVENTS_DB`).
|
|
429
429
|
|
|
430
430
|
Refresh rule keys:
|
|
431
431
|
|
|
432
|
-
- `
|
|
433
|
-
- `
|
|
432
|
+
- `findings.findings.discovery.by-sku`
|
|
433
|
+
- `findings.findings.decisions.by-sku`
|
|
434
434
|
|
|
435
435
|
### Pilot: asset-side vulnerability analysis (v2)
|
|
436
436
|
|
|
437
|
-
Snapshot-mediated flow (source is **
|
|
437
|
+
Snapshot-mediated flow (source is **items**, not findings):
|
|
438
438
|
|
|
439
|
-
1. Read source `
|
|
440
|
-
2. Match `
|
|
439
|
+
1. Read source `items-snapshots.data.skus` (also `data.sku`, `data.ip_address`).
|
|
440
|
+
2. Match `findings-snapshots` by `data.sku` / `concept._extIdentifier.sku`.
|
|
441
441
|
3. Take matched snapshot `recordId`.
|
|
442
|
-
4. Fetch `
|
|
442
|
+
4. Fetch `findings-analysis` by record-id link.
|
|
443
443
|
5. Copy flat `content.data` into `associatedVulnerabilitiesAnalysis` on the asset snapshot.
|
|
444
444
|
|
|
445
445
|
| Property | Match collection | Content collection | Join key |
|
|
446
446
|
|----------|------------------|--------------------|----------|
|
|
447
|
-
| `associatedVulnerabilitiesAnalysis` | `
|
|
447
|
+
| `associatedVulnerabilitiesAnalysis` | `findings-snapshots` | `findings-analysis` | `data.skus` → snapshot `data.sku` → `recordId` |
|
|
448
448
|
|
|
449
|
-
Target source collection: `
|
|
449
|
+
Target source collection: `items-snapshots` (`MEMORIX_ENTITIES_DB`).
|
|
450
450
|
|
|
451
|
-
Refresh rule key: `
|
|
451
|
+
Refresh rule key: `items.findings.analysis.by-sku`
|
|
452
452
|
|
|
453
453
|
---
|
|
454
454
|
|
|
@@ -58,7 +58,7 @@ type MemorixSnapshotRecord = {
|
|
|
58
58
|
|
|
59
59
|
This is the rich, readable, consumption-friendly version of the object.
|
|
60
60
|
|
|
61
|
-
Other content types under the same object type do not need to carry all of these blocks. Memorix already stores records by object type and content type, for example `
|
|
61
|
+
Other content types under the same object type do not need to carry all of these blocks. Memorix already stores records by object type and content type, for example `items-snapshots` or `findings-snapshots`.
|
|
62
62
|
|
|
63
63
|
So the rule is:
|
|
64
64
|
|
|
@@ -81,15 +81,15 @@ Not every collection under an object type should look like a full snapshot.
|
|
|
81
81
|
For example:
|
|
82
82
|
|
|
83
83
|
```txt id="evq5gl"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
items-snapshots
|
|
85
|
+
items-events
|
|
86
|
+
items-facts
|
|
87
|
+
items-chunks
|
|
88
|
+
items-history
|
|
89
|
+
items-profiles
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Only `
|
|
92
|
+
Only `items-snapshots` needs the full record surface.
|
|
93
93
|
|
|
94
94
|
Other content types should have the required Memorix structure, but not unnecessary blocks.
|
|
95
95
|
|
|
@@ -141,7 +141,7 @@ For a CVE record:
|
|
|
141
141
|
```json id="lqeccc"
|
|
142
142
|
{
|
|
143
143
|
"data": {
|
|
144
|
-
"
|
|
144
|
+
"docId": "DOC-9",
|
|
145
145
|
"severity": "critical",
|
|
146
146
|
"description": "Unrestricted file upload vulnerability in WSO2 products.",
|
|
147
147
|
"publishedAt": "2022-04-18"
|
|
@@ -161,9 +161,9 @@ These are facts about the CVE itself.
|
|
|
161
161
|
|
|
162
162
|
It is not the record’s native truth, but it is very useful for consumption.
|
|
163
163
|
|
|
164
|
-
For example, a CVE record may need to show the
|
|
164
|
+
For example, a CVE record may need to show the items affected by that CVE.
|
|
165
165
|
|
|
166
|
-
Those
|
|
166
|
+
Those items are not part of the CVE itself, so they should not live in `data`.
|
|
167
167
|
|
|
168
168
|
But consumers still need them.
|
|
169
169
|
|
|
@@ -175,12 +175,12 @@ So they belong in `associatedData`.
|
|
|
175
175
|
"affectedAssets": [
|
|
176
176
|
{
|
|
177
177
|
"assetName": "prod-api-01",
|
|
178
|
-
"
|
|
178
|
+
"sku": "10.100.18.64",
|
|
179
179
|
"recordId": "entity:asset:10.100.18.64"
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"assetName": "prod-db-02",
|
|
183
|
-
"
|
|
183
|
+
"sku": "10.100.18.65",
|
|
184
184
|
"recordId": "entity:asset:10.100.18.65"
|
|
185
185
|
}
|
|
186
186
|
]
|
|
@@ -188,7 +188,7 @@ So they belong in `associatedData`.
|
|
|
188
188
|
}
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
This gives consumers the useful joined view without pretending that affected
|
|
191
|
+
This gives consumers the useful joined view without pretending that affected items are native CVE fields.
|
|
192
192
|
|
|
193
193
|
### Rule
|
|
194
194
|
|
|
@@ -209,15 +209,15 @@ It is used when the record has links to lists of other records, and Memorix want
|
|
|
209
209
|
For example:
|
|
210
210
|
|
|
211
211
|
```txt id="omk77f"
|
|
212
|
-
CVE -> affected
|
|
213
|
-
asset -> related
|
|
212
|
+
CVE -> affected items
|
|
213
|
+
asset -> related findings
|
|
214
214
|
document -> exposed users
|
|
215
215
|
user -> accessible sensitive files
|
|
216
216
|
group -> member users
|
|
217
217
|
policy -> affected resources
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
If a CVE is linked to many affected
|
|
220
|
+
If a CVE is linked to many affected items, then `analytics.affectedAssets` contains numbers about that list.
|
|
221
221
|
|
|
222
222
|
```json id="uerbc6"
|
|
223
223
|
{
|
|
@@ -285,7 +285,7 @@ Bad:
|
|
|
285
285
|
"affectedAssets": [
|
|
286
286
|
{
|
|
287
287
|
"assetName": "prod-api-01",
|
|
288
|
-
"
|
|
288
|
+
"sku": "10.100.18.64"
|
|
289
289
|
}
|
|
290
290
|
]
|
|
291
291
|
}
|
|
@@ -300,7 +300,7 @@ Good:
|
|
|
300
300
|
"affectedAssets": [
|
|
301
301
|
{
|
|
302
302
|
"assetName": "prod-api-01",
|
|
303
|
-
"
|
|
303
|
+
"sku": "10.100.18.64"
|
|
304
304
|
}
|
|
305
305
|
]
|
|
306
306
|
},
|
|
@@ -333,15 +333,15 @@ It is the place for judgments, assessments, analysis, scores, conclusions, recom
|
|
|
333
333
|
"level": "critical",
|
|
334
334
|
"score": 92,
|
|
335
335
|
"confidence": 0.84,
|
|
336
|
-
"reason": "Critical CVE affecting production and internet-exposed
|
|
336
|
+
"reason": "Critical CVE affecting production and internet-exposed items."
|
|
337
337
|
},
|
|
338
338
|
"priority": {
|
|
339
339
|
"level": "urgent",
|
|
340
|
-
"reason": "Affected
|
|
340
|
+
"reason": "Affected items include production systems."
|
|
341
341
|
},
|
|
342
342
|
"recommendations": [
|
|
343
343
|
{
|
|
344
|
-
"action": "Patch affected
|
|
344
|
+
"action": "Patch affected items",
|
|
345
345
|
"urgency": "high"
|
|
346
346
|
}
|
|
347
347
|
]
|
|
@@ -367,7 +367,7 @@ This is an insight:
|
|
|
367
367
|
{
|
|
368
368
|
"riskScore": 92,
|
|
369
369
|
"priority": "urgent",
|
|
370
|
-
"recommendedAction": "Patch affected
|
|
370
|
+
"recommendedAction": "Patch affected items"
|
|
371
371
|
}
|
|
372
372
|
```
|
|
373
373
|
|
|
@@ -396,7 +396,7 @@ Example:
|
|
|
396
396
|
```json id="3n6qlf"
|
|
397
397
|
{
|
|
398
398
|
"data": {
|
|
399
|
-
"
|
|
399
|
+
"docId": "DOC-9",
|
|
400
400
|
"severity": "critical"
|
|
401
401
|
},
|
|
402
402
|
|
|
@@ -404,11 +404,11 @@ Example:
|
|
|
404
404
|
"affectedAssets": [
|
|
405
405
|
{
|
|
406
406
|
"assetName": "prod-api-01",
|
|
407
|
-
"
|
|
407
|
+
"sku": "10.100.18.64"
|
|
408
408
|
},
|
|
409
409
|
{
|
|
410
410
|
"assetName": "prod-db-02",
|
|
411
|
-
"
|
|
411
|
+
"sku": "10.100.18.65"
|
|
412
412
|
}
|
|
413
413
|
]
|
|
414
414
|
},
|
|
@@ -424,7 +424,7 @@ Example:
|
|
|
424
424
|
"insights": {
|
|
425
425
|
"risk": {
|
|
426
426
|
"level": "critical",
|
|
427
|
-
"reason": "The CVE affects production
|
|
427
|
+
"reason": "The CVE affects production items, including one internet-exposed asset."
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
}
|
|
@@ -466,7 +466,7 @@ Example:
|
|
|
466
466
|
"target": {
|
|
467
467
|
"recordId": "entity:asset:10.100.18.64",
|
|
468
468
|
"target": "entity",
|
|
469
|
-
"objectType": "
|
|
469
|
+
"objectType": "items"
|
|
470
470
|
},
|
|
471
471
|
"display": {
|
|
472
472
|
"name": "prod-api-01"
|
|
@@ -581,10 +581,10 @@ resolve → bounded runtime context (jobMemory.context)
|
|
|
581
581
|
|
|
582
582
|
Join paths use the same dot-path convention as descriptors: from document root, typically under `data.*`.
|
|
583
583
|
|
|
584
|
-
Example operator flow for `
|
|
584
|
+
Example operator flow for `items → locations`:
|
|
585
585
|
|
|
586
586
|
```http
|
|
587
|
-
GET /api/hippox/join-discovery?sourceObjectType=
|
|
587
|
+
GET /api/hippox/join-discovery?sourceObjectType=items&targetObjectType=locations
|
|
588
588
|
POST /api/hippox/links/simulate
|
|
589
589
|
POST /api/hippox/links/:id/approve
|
|
590
590
|
POST /api/hippox/context/resolve
|
|
@@ -647,8 +647,8 @@ The full-scale shape should be expected mainly in snapshot collections.
|
|
|
647
647
|
Example:
|
|
648
648
|
|
|
649
649
|
```txt id="ot7c3i"
|
|
650
|
-
|
|
651
|
-
|
|
650
|
+
findings-snapshots
|
|
651
|
+
items-snapshots
|
|
652
652
|
users-snapshots
|
|
653
653
|
documents-snapshots
|
|
654
654
|
```
|
|
@@ -671,10 +671,10 @@ Other content types should stay smaller and purpose-specific.
|
|
|
671
671
|
Example:
|
|
672
672
|
|
|
673
673
|
```txt id="i0znxj"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
674
|
+
findings-events
|
|
675
|
+
findings-facts
|
|
676
|
+
findings-chunks
|
|
677
|
+
findings-history
|
|
678
678
|
```
|
|
679
679
|
|
|
680
680
|
These may only need:
|
|
@@ -705,10 +705,10 @@ Other content types are specialized records.
|
|
|
705
705
|
"_id": "667f00000000000000000001",
|
|
706
706
|
|
|
707
707
|
"identity": {
|
|
708
|
-
"recordId": "entity:cve:
|
|
708
|
+
"recordId": "entity:cve:DOC-9",
|
|
709
709
|
"target": "entity",
|
|
710
|
-
"targetId": "entity:cve:
|
|
711
|
-
"objectType": "
|
|
710
|
+
"targetId": "entity:cve:DOC-9",
|
|
711
|
+
"objectType": "findings",
|
|
712
712
|
"contentType": "snapshots",
|
|
713
713
|
"version": "memorix-record.2.0"
|
|
714
714
|
},
|
|
@@ -722,14 +722,14 @@ Other content types are specialized records.
|
|
|
722
722
|
|
|
723
723
|
"concept": {
|
|
724
724
|
"kind": "cve",
|
|
725
|
-
"name": "
|
|
726
|
-
"title": "
|
|
725
|
+
"name": "DOC-9",
|
|
726
|
+
"title": "DOC-9",
|
|
727
727
|
"summary": "Critical unrestricted file upload vulnerability affecting WSO2 products.",
|
|
728
|
-
"keywords": ["
|
|
728
|
+
"keywords": ["DOC-9", "WSO2", "critical"]
|
|
729
729
|
},
|
|
730
730
|
|
|
731
731
|
"data": {
|
|
732
|
-
"
|
|
732
|
+
"docId": "DOC-9",
|
|
733
733
|
"severity": "critical",
|
|
734
734
|
"description": "Unrestricted file upload vulnerability affecting WSO2 products.",
|
|
735
735
|
"publishedAt": "2022-04-18"
|
|
@@ -739,12 +739,12 @@ Other content types are specialized records.
|
|
|
739
739
|
"affectedAssets": [
|
|
740
740
|
{
|
|
741
741
|
"assetName": "prod-api-01",
|
|
742
|
-
"
|
|
742
|
+
"sku": "10.100.18.64",
|
|
743
743
|
"recordId": "entity:asset:10.100.18.64"
|
|
744
744
|
},
|
|
745
745
|
{
|
|
746
746
|
"assetName": "prod-db-02",
|
|
747
|
-
"
|
|
747
|
+
"sku": "10.100.18.65",
|
|
748
748
|
"recordId": "entity:asset:10.100.18.65"
|
|
749
749
|
}
|
|
750
750
|
]
|
|
@@ -768,7 +768,7 @@ Other content types are specialized records.
|
|
|
768
768
|
"level": "critical",
|
|
769
769
|
"score": 92,
|
|
770
770
|
"confidence": 0.84,
|
|
771
|
-
"reason": "The CVE affects production
|
|
771
|
+
"reason": "The CVE affects production items, including one internet-exposed asset."
|
|
772
772
|
},
|
|
773
773
|
"priority": {
|
|
774
774
|
"level": "urgent",
|
|
@@ -776,7 +776,7 @@ Other content types are specialized records.
|
|
|
776
776
|
},
|
|
777
777
|
"recommendations": [
|
|
778
778
|
{
|
|
779
|
-
"action": "Patch affected
|
|
779
|
+
"action": "Patch affected items",
|
|
780
780
|
"urgency": "high"
|
|
781
781
|
}
|
|
782
782
|
]
|
|
@@ -784,9 +784,9 @@ Other content types are specialized records.
|
|
|
784
784
|
|
|
785
785
|
"index": {
|
|
786
786
|
"text": {
|
|
787
|
-
"title": "
|
|
787
|
+
"title": "DOC-9",
|
|
788
788
|
"summary": "Critical unrestricted file upload vulnerability affecting WSO2 products.",
|
|
789
|
-
"keywords": ["
|
|
789
|
+
"keywords": ["DOC-9", "critical", "prod-api-01", "prod-db-02"]
|
|
790
790
|
},
|
|
791
791
|
"facets": {
|
|
792
792
|
"severity": "critical",
|
|
@@ -801,7 +801,7 @@ Other content types are specialized records.
|
|
|
801
801
|
"target": {
|
|
802
802
|
"recordId": "entity:asset:10.100.18.64",
|
|
803
803
|
"target": "entity",
|
|
804
|
-
"objectType": "
|
|
804
|
+
"objectType": "items"
|
|
805
805
|
},
|
|
806
806
|
"display": {
|
|
807
807
|
"name": "prod-api-01"
|
|
@@ -815,7 +815,7 @@ Other content types are specialized records.
|
|
|
815
815
|
"target": {
|
|
816
816
|
"recordId": "entity:asset:10.100.18.65",
|
|
817
817
|
"target": "entity",
|
|
818
|
-
"objectType": "
|
|
818
|
+
"objectType": "items"
|
|
819
819
|
},
|
|
820
820
|
"display": {
|
|
821
821
|
"name": "prod-db-02"
|
|
@@ -840,7 +840,7 @@ Other content types are specialized records.
|
|
|
840
840
|
|
|
841
841
|
"_system": {
|
|
842
842
|
"memorix": {
|
|
843
|
-
"entity": "
|
|
843
|
+
"entity": "findings",
|
|
844
844
|
"contentType": "snapshots",
|
|
845
845
|
"operation": "upsert"
|
|
846
846
|
},
|
|
@@ -911,11 +911,11 @@ full record-level index projections
|
|
|
911
911
|
So for example:
|
|
912
912
|
|
|
913
913
|
```txt id="cpyqdi"
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
914
|
+
findings-snapshots ✅ full record shape
|
|
915
|
+
findings-events ❌ not full shape
|
|
916
|
+
findings-facts ❌ not full shape
|
|
917
|
+
findings-chunks ❌ not full shape
|
|
918
|
+
findings-history ❌ not full shape
|
|
919
919
|
```
|
|
920
920
|
|
|
921
921
|
The clean rule:
|
|
@@ -16,7 +16,7 @@ source snapshot (join value from data / identifier / identity)
|
|
|
16
16
|
→ copy flat content.data into associated<ObjectType><ContentType>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
**Never** join a source snapshot directly to a non-snapshot collection by business keys such as `data.
|
|
19
|
+
**Never** join a source snapshot directly to a non-snapshot collection by business keys such as `data.sku`.
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -24,9 +24,9 @@ source snapshot (join value from data / identifier / identity)
|
|
|
24
24
|
|
|
25
25
|
### Cover / Chapter 0 — Vulnerability patch framing
|
|
26
26
|
|
|
27
|
-
**Incorrect:** The vulnerability patch wires `
|
|
27
|
+
**Incorrect:** The vulnerability patch wires `findings-snapshots` directly to `findings-discovery` / `findings-decisions` by `data.sku`.
|
|
28
28
|
|
|
29
|
-
**Correct:** The patch enriches `
|
|
29
|
+
**Correct:** The patch enriches `findings-snapshots` using snapshot-mediated resolution: match snapshots first (including same-object `findings-snapshots`), then fetch discovery/decisions content linked to the matched snapshot `recordId`.
|
|
30
30
|
|
|
31
31
|
### Chapter 4 — Core association rules
|
|
32
32
|
|
|
@@ -38,10 +38,10 @@ source snapshot (join value from data / identifier / identity)
|
|
|
38
38
|
|
|
39
39
|
Replace the direct-join walkthrough with:
|
|
40
40
|
|
|
41
|
-
1. Read source `
|
|
42
|
-
2. Match target `
|
|
41
|
+
1. Read source `findings-snapshots.data.sku`.
|
|
42
|
+
2. Match target `findings-snapshots` by snapshot paths such as `data.sku`, `concept._extIdentifier`, or `identity`.
|
|
43
43
|
3. Take matched target snapshot `recordId`.
|
|
44
|
-
4. Fetch `
|
|
44
|
+
4. Fetch `findings-discovery` / `findings-decisions` by a record-id link field such as `data.sourceRecordId`, `concept._extIdentifier.recordId`, or root `recordId`.
|
|
45
45
|
5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
|
|
46
46
|
|
|
47
47
|
Payload rules for this pilot:
|
|
@@ -71,10 +71,10 @@ Payload rules for this pilot:
|
|
|
71
71
|
## Replacement example (vulnerability discovery/decisions)
|
|
72
72
|
|
|
73
73
|
```markdown
|
|
74
|
-
1. Read source `
|
|
75
|
-
2. Match target `
|
|
74
|
+
1. Read source `findings-snapshots.data.sku`.
|
|
75
|
+
2. Match target `findings-snapshots` by snapshot paths such as `data.sku`, `concept._extIdentifier`, or `identity`.
|
|
76
76
|
3. Take matched target snapshot `recordId`.
|
|
77
|
-
4. Fetch `
|
|
77
|
+
4. Fetch `findings-discovery` / `findings-decisions` by a record-id link field such as `data.sourceRecordId`, `concept._extIdentifier.recordId`, or root `recordId`.
|
|
78
78
|
5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -84,7 +84,7 @@ Payload rules for this pilot:
|
|
|
84
84
|
|
|
85
85
|
| Aspect | Incorrect (PDF Chapter 5) | Correct |
|
|
86
86
|
|--------|---------------------------|---------|
|
|
87
|
-
| Join path | `
|
|
87
|
+
| Join path | `findings-snapshots.data.sku` → `findings-discovery.data.sku` | `findings-snapshots.data.sku` → `findings-snapshots` (snapshot match) → `recordId` → `findings-discovery` (by `concept._extIdentifier.recordId`) |
|
|
88
88
|
| Phase count | One-phase direct content join | Two-phase: snapshot match, then content fetch |
|
|
89
89
|
| Same-object rule | Exception allowing direct snapshot→content | Same-object still matches snapshots first |
|
|
90
90
|
| Bridge pattern | Implicit direct join | Explicit `match` + `content` in associator rules |
|
|
@@ -95,15 +95,15 @@ Payload rules for this pilot:
|
|
|
95
95
|
|
|
96
96
|
```json
|
|
97
97
|
{
|
|
98
|
-
"sourceJoinPaths": ["data.
|
|
98
|
+
"sourceJoinPaths": ["data.sku"],
|
|
99
99
|
"match": {
|
|
100
|
-
"collection": "
|
|
101
|
-
"joinPaths": ["data.
|
|
100
|
+
"collection": "findings-snapshots",
|
|
101
|
+
"joinPaths": ["data.sku", "concept._extIdentifier.sku", "identity", "recordId"]
|
|
102
102
|
},
|
|
103
103
|
"content": {
|
|
104
|
-
"collection": "
|
|
104
|
+
"collection": "findings-discovery",
|
|
105
105
|
"contentType": "discovery",
|
|
106
|
-
"linkPaths": ["concept.
|
|
106
|
+
"linkPaths": ["concept._extIdentifier.recordId", "data.sourceRecordId", "recordId"]
|
|
107
107
|
},
|
|
108
108
|
"associatedProperty": "associatedVulnerabilitiesDiscovery",
|
|
109
109
|
"payload": { "format": "v2", "source": "target.data", "includeRecordId": false }
|
package/docs/memorix-format.md
CHANGED
|
@@ -55,33 +55,29 @@ Every piece of business data lives in **exactly one** of these buckets (plus the
|
|
|
55
55
|
|
|
56
56
|
| Bucket | Contents |
|
|
57
57
|
|---|---|
|
|
58
|
-
| **`data`** |
|
|
59
|
-
| **`analytics`** |
|
|
60
|
-
| **`enrichment`** | External
|
|
61
|
-
| **`insights`** |
|
|
58
|
+
| **`data`** | Native facts about the subject itself |
|
|
59
|
+
| **`analytics`** | Numeric rollups computed from other records |
|
|
60
|
+
| **`enrichment`** | External / prior-system values as received |
|
|
61
|
+
| **`insights`** | Judgments, scores, recommendations |
|
|
62
62
|
| **`associatedData`** | Linked snapshot/data content copied from associated records' `data` payloads |
|
|
63
63
|
| **`associatedDiscovery`** | Linked discovery content copied from associated records' `data` payloads |
|
|
64
64
|
| **`associatedAnalysis`** | Linked analysis content copied from associated records' `data` payloads |
|
|
65
|
-
| **`associatedSiblings`** | Self-association: other snapshot `data` payloads sharing the same join key
|
|
66
|
-
| **`associatedChildren`** | Parent-to-child association
|
|
65
|
+
| **`associatedSiblings`** | Self-association: other snapshot `data` payloads sharing the same join key; each item includes linked `recordId`. Default cap: 99 items. |
|
|
66
|
+
| **`associatedChildren`** | Parent-to-child association: child `data` payloads whose join key matches the parent; each item includes linked `recordId`. Default cap: 99 items. |
|
|
67
67
|
|
|
68
68
|
Do not duplicate a value across buckets. When reshaping legacy records, promote `sourceFacts` values into the correct bucket rather than wrapping them.
|
|
69
69
|
|
|
70
|
-
Associated properties are always arrays and are omitted when no linked data was resolved. Do not write empty arrays, and do not switch between object and array shape. Custom associated content uses the same root-level pattern: `associatedCompliance`, `associatedBusinessContext`,
|
|
70
|
+
Associated properties are always arrays and are omitted when no linked data was resolved. Do not write empty arrays, and do not switch between object and array shape. Custom associated content uses the same root-level pattern: `associatedCompliance`, `associatedBusinessContext`, and so on.
|
|
71
71
|
|
|
72
|
-
**Ranked top associations** use `associatedTop<ObjectType><ContentType>` (e.g. `
|
|
72
|
+
**Ranked top associations** use `associatedTop<ObjectType><ContentType>` (e.g. `associatedTopItemsAnalysis`). Candidates are sorted by the linked object type’s `importanceIndicator` (or rule `rankBy`) and capped (default 5). See [association-format.md](./association-format.md).
|
|
73
73
|
|
|
74
|
-
**Enrichment associations** use `associated<ObjectType>Enrichment` (e.g. `
|
|
74
|
+
**Enrichment associations** use `associated<ObjectType>Enrichment` (e.g. `associatedItemGroupsEnrichment`). They copy the linked snapshot’s root `enrichment` bucket as a flat object array — not a separate `*-enrichment` content collection. See [association-format.md](./association-format.md).
|
|
75
75
|
|
|
76
76
|
Associated items contain the linked record's `data` payload (or `enrichment` / `concept` / root property for the specialized forms). Legacy associations may also include inline `recordId` and `contentType`; v2 associations store traceability in optional item-level `_system.association` metadata. Do not wrap associated items with redundant envelope keys; failure and diagnostic details belong in `<objectType>-failed` collections instead.
|
|
77
77
|
|
|
78
78
|
**Associated property metadata:** Consumers should prefer Memorix associated-property metadata when available (`@x12i/memorix-associator`). Associated snapshot properties are exposed through managed descriptors plus discovery. Until metadata is available, or when reading older/custom records, fall back to the prefix rule: root-level fields on snapshots whose names start with `associated`. The well-known names `associatedData`, `associatedDiscovery`, and `associatedAnalysis` are conventions only — the complete set comes from metadata plus the `associated*` fallback rule.
|
|
79
79
|
|
|
80
|
-
When a field can naturally have more than one value, processing contracts use array shape even if the current record has only one value.
|
|
81
|
-
|
|
82
|
-
For subnets, `data.cidr` is the canonical join key. Subnet records also carry structured `data.subnet` plus flattened `data.networkAddress`, `data.prefixLength`, and `data.mask`, all derived deterministically from `data.cidr`.
|
|
83
|
-
|
|
84
|
-
Assets also carry `data.assetIpClassifications[]` when topology resolution runs. Each item records `{ ip, scope, confidence, reason, reasoning, matchedSubnetCidr?, matchedSubnetIp? }`, where `scope` is `inside`, `outside`, or `unknown`. Convenience arrays `data.insideAssetIps`, `data.outsideAssetIps`, and `data.unknownAssetIps` may be populated from those classifications. `inside` means the IP either matched canonical topology with high confidence, or is RFC1918 private space without a matching topology CIDR with medium confidence. `outside` means public/non-RFC1918 with no topology match. `unknown` covers invalid, loopback, and link-local addresses.
|
|
80
|
+
When a field can naturally have more than one value, processing contracts use array shape even if the current record has only one value. Join-key recipes (which paths and shapes) are defined on **entity descriptors** for each Catalox object type — not in this format package.
|
|
85
81
|
|
|
86
82
|
---
|
|
87
83
|
|
|
@@ -89,49 +85,45 @@ Assets also carry `data.assetIpClassifications[]` when topology resolution runs.
|
|
|
89
85
|
|
|
90
86
|
```ts
|
|
91
87
|
type MemorixConcept = {
|
|
92
|
-
|
|
88
|
+
_extIdentifier: {
|
|
93
89
|
kind: string;
|
|
94
90
|
/** Primary value — property name matches `kind`. */
|
|
95
91
|
[kind: string]: string;
|
|
96
92
|
/**
|
|
97
93
|
* Deterministic join key: `kind` first, then the kind-named value,
|
|
98
94
|
* then any other scalar fields in sorted key order, joined by `:`.
|
|
99
|
-
* See `.operational/fr/2026-07-09-memorix-format-identifier-key.md`.
|
|
100
95
|
*/
|
|
101
|
-
|
|
96
|
+
key: string;
|
|
102
97
|
};
|
|
103
98
|
title: string;
|
|
104
99
|
};
|
|
105
100
|
```
|
|
106
101
|
|
|
107
|
-
`
|
|
102
|
+
`_extIdentifier` is a discriminated object: `kind` names the id type, and a same-named property holds the value. `key` is required. How `kind` / title are derived for an object type is defined on the **entity descriptor**, not here.
|
|
108
103
|
|
|
109
104
|
```json
|
|
110
105
|
"concept": {
|
|
111
|
-
"
|
|
112
|
-
"kind": "
|
|
113
|
-
"
|
|
114
|
-
"
|
|
106
|
+
"_extIdentifier": {
|
|
107
|
+
"kind": "sku",
|
|
108
|
+
"sku": "SKU-100",
|
|
109
|
+
"key": "sku:SKU-100"
|
|
115
110
|
},
|
|
116
|
-
"title": "
|
|
111
|
+
"title": "Blue widget"
|
|
117
112
|
}
|
|
118
113
|
```
|
|
119
114
|
|
|
120
|
-
`concept.name` is not used. `title` is the human-readable display name; it may differ from the identifier value
|
|
115
|
+
`concept.name` is not used. `concept.identifier` / `identifierKey` are legacy — migrate to `_extIdentifier` / `key`. `title` is the human-readable display name; it may differ from the identifier value.
|
|
121
116
|
|
|
122
|
-
### Identifier `kind`
|
|
117
|
+
### Identifier `kind` (illustrative — not format law)
|
|
123
118
|
|
|
124
|
-
| Object type | `kind` | Example
|
|
119
|
+
| Object type (abstract) | `kind` | Example |
|
|
125
120
|
|---|---|---|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
| Topology zones | `zoneName` | `{"kind":"zoneName","zoneName":"LAN","identifierKey":"zoneName:LAN"}` |
|
|
131
|
-
| Topology VRs | `vrName` | `{"kind":"vrName","vrName":"VR-UPB-MED","identifierKey":"vrName:VR-UPB-MED"}` |
|
|
132
|
-
| Users | `sessionKey` | `{"kind":"sessionKey","sessionKey":"med\\cap\|10.39.44.205\|UIA","identifierKey":"sessionKey:med\\cap\|10.39.44.205\|UIA"}` |
|
|
121
|
+
| Items | `sku` | `{"kind":"sku","sku":"SKU-100","key":"sku:SKU-100"}` |
|
|
122
|
+
| Groups | `groupCode` | `{"kind":"groupCode","groupCode":"G-42","key":"groupCode:G-42"}` |
|
|
123
|
+
| Locations | `locationCode` | `{"kind":"locationCode","locationCode":"WH-A","key":"locationCode:WH-A"}` |
|
|
124
|
+
| Documents | `docId` | `{"kind":"docId","docId":"DOC-9","key":"docId:DOC-9"}` |
|
|
133
125
|
|
|
134
|
-
|
|
126
|
+
Domain packs (Catalox) may use different `kind` values; those recipes live on descriptors.
|
|
135
127
|
|
|
136
128
|
---
|
|
137
129
|
|
|
@@ -143,6 +135,7 @@ System metadata — provenance, processing history, graph-run stamps — never b
|
|
|
143
135
|
type MemorixLifecycleEvent = { process: string; processedAt: string };
|
|
144
136
|
|
|
145
137
|
type MemorixSystemBlock = {
|
|
138
|
+
state?: "active" | "archived" | "superseded";
|
|
146
139
|
lifecycle?: MemorixLifecycleEvent[];
|
|
147
140
|
_graphRuns?: Record<string, GraphRunEntry>;
|
|
148
141
|
_jobTypeRuns?: Record<string, GraphRunEntry>;
|
|
@@ -164,8 +157,8 @@ Root-level `lifecycle` is gone. Processing events live at `_system.lifecycle`:
|
|
|
164
157
|
- Each entry is `{ process, processedAt }`.
|
|
165
158
|
- When `createdAt`, `modifiedAt`, and `capturedAt` are identical (the common snapshot case), emit a single `"enriched"` event rather than three duplicate timestamps.
|
|
166
159
|
- Distinct events (e.g. a future re-enrichment) append as additional array entries.
|
|
167
|
-
- `
|
|
168
|
-
-
|
|
160
|
+
- Omit `lifecycle` entirely when no timestamp is known — do not emit an empty array.
|
|
161
|
+
- `_system.state` is the present-tense lifecycle state enum (`active` \| `archived` \| `superseded`).
|
|
169
162
|
|
|
170
163
|
`_system.provenance.enrichedAt` (when present) is separate provenance metadata about a prior enrichment step, not a substitute for `lifecycle`.
|
|
171
164
|
|
|
@@ -179,102 +172,59 @@ Dot-path references: `_system._graphRuns.<graphId>`, `_system._jobTypeRuns.<jobT
|
|
|
179
172
|
|
|
180
173
|
## Complete example
|
|
181
174
|
|
|
182
|
-
|
|
175
|
+
Abstract item snapshot showing envelope fields and open buckets:
|
|
183
176
|
|
|
184
177
|
```json
|
|
185
178
|
{
|
|
186
|
-
"
|
|
187
|
-
"recordId": "69d216ed358778e93215787e",
|
|
179
|
+
"recordId": "rec-item-1",
|
|
188
180
|
"concept": {
|
|
189
|
-
"
|
|
190
|
-
"kind": "
|
|
191
|
-
"
|
|
192
|
-
"
|
|
181
|
+
"_extIdentifier": {
|
|
182
|
+
"kind": "sku",
|
|
183
|
+
"sku": "SKU-100",
|
|
184
|
+
"key": "sku:SKU-100"
|
|
193
185
|
},
|
|
194
|
-
"title": "
|
|
186
|
+
"title": "Blue widget"
|
|
195
187
|
},
|
|
196
188
|
"data": {
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"cveId": "CVE-2020-14882",
|
|
201
|
-
"severity": "critical",
|
|
202
|
-
"pluginId": 142594
|
|
189
|
+
"sku": "SKU-100",
|
|
190
|
+
"color": "blue",
|
|
191
|
+
"family": "widgets"
|
|
203
192
|
},
|
|
204
193
|
"associatedData": [
|
|
205
|
-
{ "
|
|
206
|
-
{ "
|
|
194
|
+
{ "locationCode": "WH-A", "qty": 12 },
|
|
195
|
+
{ "locationCode": "WH-B", "qty": 3 }
|
|
207
196
|
],
|
|
208
197
|
"analytics": {
|
|
209
|
-
"
|
|
198
|
+
"linkedLocations": { "total": 2 }
|
|
210
199
|
},
|
|
211
200
|
"enrichment": {
|
|
212
|
-
"
|
|
213
|
-
|
|
214
|
-
"cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
|
|
215
|
-
},
|
|
216
|
-
"epss": { "epss": 0.94454, "percentile": 0.99992 },
|
|
217
|
-
"kev": { "isKnownExploited": true },
|
|
218
|
-
"compositeScore": 0.974,
|
|
219
|
-
"riskLevel": "CRITICAL",
|
|
220
|
-
"vprScore": "9.2"
|
|
201
|
+
"catalogScore": 0.91,
|
|
202
|
+
"sourceSystem": "erp-feed"
|
|
221
203
|
},
|
|
222
204
|
"insights": {
|
|
223
|
-
"
|
|
224
|
-
"potentialImpact": { "level": "CRITICAL", "confidence": 0.88 }
|
|
205
|
+
"priority": { "level": "high", "confidence": 0.9 }
|
|
225
206
|
},
|
|
226
207
|
"_system": {
|
|
208
|
+
"state": "active",
|
|
227
209
|
"provenance": {
|
|
228
|
-
"sourceId": "6914a132880706529ece3da1",
|
|
229
|
-
"snapshotRecordId": "69d216ed358778e93215787e",
|
|
230
210
|
"source": {
|
|
231
|
-
"database": "
|
|
232
|
-
"collection": "
|
|
233
|
-
"server": "default"
|
|
211
|
+
"database": "example-db",
|
|
212
|
+
"collection": "items-rawdata"
|
|
234
213
|
}
|
|
235
214
|
},
|
|
236
215
|
"lifecycle": [
|
|
237
216
|
{ "process": "enriched", "processedAt": "2026-04-05T08:01:15.511Z" }
|
|
238
|
-
]
|
|
239
|
-
"_graphRuns": {
|
|
240
|
-
"graph-t92669z": {
|
|
241
|
-
"status": "done",
|
|
242
|
-
"startedAt": "2026-07-01T23:41:23.233Z",
|
|
243
|
-
"completedAt": "2026-07-01T23:42:05.604Z",
|
|
244
|
-
"lastJobRunId": "f7af7f7a-0c5c-4825-9282-d39bea836682",
|
|
245
|
-
"attempts": 1,
|
|
246
|
-
"result": {
|
|
247
|
-
"target": "entity",
|
|
248
|
-
"entityName": "vulnerabilities-groups",
|
|
249
|
-
"contentType": "inferences",
|
|
250
|
-
"recordId": "75de9244-8d44-43b8-8698-7e22621b1087"
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
217
|
+
]
|
|
254
218
|
}
|
|
255
219
|
}
|
|
256
220
|
```
|
|
257
221
|
|
|
258
|
-
Not every record populates every bucket.
|
|
222
|
+
Not every record populates every bucket. Non-snapshot content types usually keep substance in `.data` only (SCOPE).
|
|
259
223
|
|
|
260
224
|
---
|
|
261
225
|
|
|
262
226
|
## Migration notes
|
|
263
227
|
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
-
|
|
267
|
-
|
|
268
|
-
### Subnets / topology subnets merge (2026-07-05)
|
|
269
|
-
|
|
270
|
-
`topology-subnets` was merged into `subnets`. Canonical `subnets` records are topology
|
|
271
|
-
CIDR segments (`concept.identifier.kind: "cidr"`), spread across content types
|
|
272
|
-
`snapshots` / `core` / `inferences` / `scoped`. A subset of the old, pre-merge `subnets`
|
|
273
|
-
records (subnetIp-only, no CIDR) had zero overlap with any canonical CIDR and were
|
|
274
|
-
quarantined rather than dropped — they live in the `legacy` content type
|
|
275
|
-
(`subnets-legacy`), keyed by `concept.identifier.kind: "subnetIp"`, with an explicit
|
|
276
|
-
`_system.quarantine` block (`legacy: true`, `status`, `reason`) marking them as not
|
|
277
|
-
eligible for `assets -> subnets` joins. `assets.data.subnetCidr` (precomputed via CIDR
|
|
278
|
-
containment against canonical `subnets-snapshots`) is the join key used for that
|
|
279
|
-
relation — see `scripts/memorix-subnets-merge/` for the migration and backfill scripts,
|
|
280
|
-
and `temp/part-b-implementation-design-2026-07-05T06-20-00Z.md` for the full design.
|
|
228
|
+
- Prefer **`npx memorix-format migrate`** for transforms (dry-run → `--out` → validate). Live Mongo apply is a separate ops step.
|
|
229
|
+
- Historical reshape scripts under `scripts/` are not the recommended path.
|
|
230
|
+
- Thin envelope helpers may still be re-exported from `@x12i/memorix-descriptors`; **format packages are the SOT**.
|
|
@@ -73,7 +73,7 @@ type WorkflowTask = {
|
|
|
73
73
|
|
|
74
74
|
Catalog id: `memorix-workflow-field-schemas`
|
|
75
75
|
|
|
76
|
-
One item per object type (natural id e.g. `
|
|
76
|
+
One item per object type (natural id e.g. `items-workflow-fields`):
|
|
77
77
|
|
|
78
78
|
```ts
|
|
79
79
|
type WorkflowFieldSchemaItem = {
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"recordId": "
|
|
2
|
+
"recordId": "rec-item-1",
|
|
3
3
|
"concept": {
|
|
4
4
|
"_extIdentifier": {
|
|
5
|
-
"kind": "
|
|
6
|
-
"
|
|
7
|
-
"key": "
|
|
5
|
+
"kind": "sku",
|
|
6
|
+
"sku": "SKU-100",
|
|
7
|
+
"key": "sku:SKU-100"
|
|
8
8
|
},
|
|
9
|
-
"title": "
|
|
9
|
+
"title": "Blue widget"
|
|
10
10
|
},
|
|
11
11
|
"data": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
12
|
+
"sku": "SKU-100",
|
|
13
|
+
"color": "blue"
|
|
14
14
|
},
|
|
15
15
|
"associatedData": [
|
|
16
|
-
{ "
|
|
16
|
+
{ "locationCode": "WH-A", "qty": 12 }
|
|
17
17
|
],
|
|
18
18
|
"analytics": {
|
|
19
|
-
"
|
|
19
|
+
"linkedLocations": { "total": 1 }
|
|
20
20
|
},
|
|
21
21
|
"insights": {
|
|
22
|
-
"
|
|
22
|
+
"priority": { "level": "high", "confidence": 0.9 }
|
|
23
23
|
},
|
|
24
24
|
"_system": {
|
|
25
25
|
"state": "active",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"allContentTypesCoveredInContentTypesJson": true,
|
|
73
73
|
"platformOpenBucketsPresentInOpenBucketsJson": ["analytics", "enrichment", "insights"],
|
|
74
74
|
"associatedNamesFollowPrefixConvention": true,
|
|
75
|
-
"noObjectTypesInFormatLawJson": true
|
|
75
|
+
"noObjectTypesInFormatLawJson": true,
|
|
76
|
+
"conceptRecipesOnEntityDescriptors": true
|
|
76
77
|
}
|
|
77
78
|
}
|
package/package.json
CHANGED