@exellix/narrix-scoper
v2.0.0
Published
Scoper: CNI v1.1 in → facts[] + signals[] out. Rule packs over subject/content/references. Optional legacy engine + narrator mapping.
Readme
@exellix/narrix-scoper
Scoper accepts CNI v1.1 as its only input and outputs facts[] and signals[]. No adapter selection, no dataset routing, no engine pipeline — the engine is responsible for those.
Scoper API (primary)
import { scope, type ScoperPack, type ScoperResult } from "@exellix/narrix-scoper";
import type { CniV11 } from "@exellix/narrix-cni";
const pack: ScoperPack = {
schema: "scoper.pack.v1",
packId: "my.pack",
version: "1.0.0",
rules: [
{
ruleId: "has-cve",
when: { refsContain: { type: "cve" } },
emit: { signals: [{ code: "SIG_VULN_PRESENT", severity: "high" }] },
},
],
};
const result: ScoperResult = await scope(cni, pack);
// result.schema === "narrix.scoping.v1"
// result.facts — sorted by (kind, value, evidence)
// result.signals — sorted by (code, severity, evidence)
// result.diagnostics?.matchedRuleIds- Input:
scope(cni: CniV11, pack: ScoperPack) - Output:
ScoperResult = { schema, facts, signals, diagnostics? } - Rules read only
cni.subject,cni.content,cni.references,cni.meta(CNI paths). UserefsContain: { type, value?, role? }to match references. - Deterministic: facts and signals are sorted; no reliance on input order.
- Evidence in output must reference CNI (path, span with contentId, or ref with refId). See
@exellix/narrix-cniforEvidencePointerV11.
Reference vocabulary
Scoper can only scope what adapters emit. The minimum reference set that scoper rules may rely on is documented and enforced in tests. See reference-vocabulary.ts and exports:
| refKey | type / role | Description | |--------|-------------|-------------| | ref.ip.asset | ip / assetIp | Asset IP address | | ref.vuln.pluginId | pluginId / vulnId | Vulnerability plugin id | | ref.vuln.cve | cve / vulnId | CVE identifier | | ref.vuln.severity | severity | Severity level | | ref.egress.outsideHit | egress / egressOutside | Egress / internet-reachable hint | | ref.subnet.cidr | subnet.cidr | Subnet CIDR |
import { SCOPER_REFERENCE_VOCABULARY, isKnownReferenceType } from "@exellix/narrix-scoper";Legacy: engine + mapping
This package also exposes the narrative engine and narrator mapping for pipelines that build CNI from raw input and then run rules. For “scoper only” use the scope() API above.
Terminology
- Narrative Type — A question or dimension (e.g., "subnet.exposure.public-facing"). Defined by
narrativeTypeIdin rules. - Story (Narrative Instance) — The scoped answer for a specific entity+type. This is the assembled story instance produced by the engine for a given subject and narrative type.
- Narrative Outcome — A classification bucket for that story (if present). Optional classification label applied to a story instance.
See Terminology Glossary for detailed definitions and examples.
Install
npm i @exellix/narrix-scoperOptional peers (only needed if you use the legacy engine with adapters):
npm i nx-functions nx-rulesDevelopment
- Build:
npm run build - Test:
npm test - Typecheck:
npm run typecheck - Lint:
npm run lint
See API reference for the full exported surface.
API Naming
| Current name | New preferred name | Notes |
| ------------ | ------------------ | ----- |
| createNarrativeEngine() | createNarrativeEngine() | No change (engine creates stories) |
| evaluate() | evaluate() | No change (evaluates and builds stories) |
| result.narratives | result.stories | Array of Story instances (Narrative Instances) |
| CniNarrativeV1 | CniNarrativeV1 | Type name (represents a Story) |
| narrativeTypeId | narrativeTypeId | No change (identifies Narrative Type) |
| emit.narratives | emit.narratives | Rule emit field (contains Story instances) |
Note: Current code uses narratives as the key name. In terminology:
narratives[]represents stories (Narrative Instances)- Each item has a
narrativeTypeIdidentifying the Narrative Type - Optional
outcomefield would represent Narrative Outcome (not yet in schema)
Quick start (mapping + features + rules)
import {
createNarrativeEngine,
type NarratorMappingV1,
type SignalsCatalogV1,
type RulePackV1,
} from "@exellix/narrix-scoper";
const signals: SignalsCatalogV1 = {
schema: "signals.catalog.v1",
signals: {
SIG_PUBLIC_EXPOSED: {
code: "SIG_PUBLIC_EXPOSED",
title: "Public exposure",
severityDefault: "medium",
description: "Subject appears to be exposed to untrusted scope.",
evidence: { expected: ["path:isPublic", "path:ingress.pathCount"] }
}
}
};
const mapping: NarratorMappingV1 = {
schema: "narrator.mapping.v1",
mappingId: "subnet.narrator.v1",
version: "1.0.0",
subject: {
type: { const: "subnet" },
id: { path: "id" },
displayName: { path: "name" }
},
facts: [
{ kind: "FACT_SUBNET_CIDR", value: { path: "cidr" }, evidence: [{ path: "cidr" }] }
],
// features may populate baggage that rules can reference later
features: [
{ id: "net.subnet.isPublic", callFn: "net.subnet.isPublic", writeTo: "baggage.subnet.isPublic" }
]
};
const rules: RulePackV1 = {
schema: "rules.pack.v1",
packId: "subnet.rules.v1",
version: "1.0.0",
rules: [
{
ruleId: "subnet-public-exposed",
when: { eq: [{ path: "baggage.subnet.isPublic" }, { const: true }] },
emit: {
signals: [
{
code: "SIG_PUBLIC_EXPOSED",
severity: "medium",
evidence: [{ path: "isPublic" }]
}
],
narratives: [{ narrativeTypeId: "subnet.exposure.public-facing", confidence: 0.8 }] // emits Story instances
}
}
]
};
const engine = createNarrativeEngine({
signalsCatalog: signals,
// feature registry is optional; you can also run path-only mappings
featureRegistry: {
async execute(name, ctx) {
if (name === "net.subnet.isPublic") {
const isPublic = Boolean((ctx.input as any)?.isPublic);
return { passed: true, baggage: { value: isPublic } };
}
return { passed: false, baggage: { error: "unknown function" } };
}
},
rulePacks: [rules],
});
const result = await engine.evaluate({
input: { id: "subnet-123", name: "Subnet A", cidr: "10.0.1.0/24", isPublic: true },
mapping
});
console.log(result.signals);
// Terminology mapping: result.narratives are stories (Narrative Instances)
console.log(result.narratives); // Array of Story instances for this subjectInputs you can pass
- Raw object + mapping (recommended):
{ input: any, mapping: NarratorMappingV1 }
- CNI directly (if you already normalized elsewhere):
{ cni: CniV1 }
Important contracts (pack authors)
Feature → Signal boundary
Features do not emit signals. Features can:
- compute derived values and write them to
baggage.* - add “candidates” to
baggage.candidates.*(optional pattern)
Only rules emit signals (and stories). This keeps signals auditable.
Conflict resolution scope (default)
Conflicts are resolved per (itemId + subject.id + signalKey). Default merge:
- keep max severity
- union evidence pointers
- union tags
You can override merge policy in createNarrativeEngine({ conflictPolicy }).
Deprecations
The following naming conventions are being aligned with the clarified terminology model. Current code identifiers remain for backward compatibility, but documentation uses the preferred terms:
result.narratives→ represents stories (Narrative Instances). The key namenarrativesis retained in code, but semantically these are Story instances.emit.narratives→ emits Story instances for the given Narrative Type.- "Detect narratives" / "narrative detection" → prefer "Build stories" / "Assemble stories" / "Story assembly"
Note: No runtime behavior changes are planned. This is a documentation and terminology alignment only.
Adapters
nx-functions adapter
import { createNxFunctionsRegistryAdapter } from "@exellix/narrix-scoper/adapters/nx-functions";nx-rules adapter
import { createNxRulesRuleEngineAdapter } from "@exellix/narrix-scoper/adapters/nx-rules";License
MIT
