runward 0.10.0 → 0.11.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 +1 -0
- package/dist/cli.js +7 -0
- package/dist/commands/compliance.js +60 -0
- package/dist/lib/compliance.js +380 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
|
|
|
63
63
|
| `runward doctor` | Environment and installation checks |
|
|
64
64
|
| `runward update` | Refresh `runward/workflows/` from the package — mission state never touched, local edits preserved unless `--force` |
|
|
65
65
|
| `runward characterize` | **Read-only inventory** of an existing codebase (brownfield / retro-documentation) → `runward/characterization.md`: dependencies and lockfiles, entrypoints, CI, tests, git-log shape. Deterministic and zero-LLM; it parses artifacts at rest and never runs, builds, or writes to your code. Facts, not decisions — reconstructing the *why* stays yours |
|
|
66
|
+
| `runward compliance <regime>` | **Assemble a regime-framed evidence pack** (`iso-42001`, `nist-ai-rmf`, `eu-ai-act`) from the mission's artifacts → `runward/compliance/…`: a human readiness *draft* (OWASP ASI coverage, rule-conformance status, ADR journal, with the operator-only sections flagged) **and a machine-readable OSCAL** component-definition to feed your GRC/auditor tool. Deterministic, read-only, zero-LLM — supporting evidence, never a compliance claim |
|
|
66
67
|
|
|
67
68
|
Global flags: `--yes`, `--dry-run`, `--verbose`, `--no-color`. Exit codes: 0 success, 1 gaps/warnings, 2 missing prerequisite.
|
|
68
69
|
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { statusCommand } from "./commands/status.js";
|
|
|
12
12
|
import { doctorCommand } from "./commands/doctor.js";
|
|
13
13
|
import { updateCommand } from "./commands/update.js";
|
|
14
14
|
import { characterizeCommand } from "./commands/characterize.js";
|
|
15
|
+
import { complianceCommand } from "./commands/compliance.js";
|
|
15
16
|
// Exit codes: 0 = success · 1 = gaps/warnings · 2 = missing prerequisite
|
|
16
17
|
process.on("uncaughtException", (err) => {
|
|
17
18
|
if (err.name === "ExitPromptError")
|
|
@@ -83,4 +84,10 @@ program
|
|
|
83
84
|
.option("-p, --path <path>", "project directory (default: .)")
|
|
84
85
|
.option("--mine", "also propose candidate retroactive ADRs as DRAFT hypotheses (deterministic git archaeology, no model call)")
|
|
85
86
|
.action(characterizeCommand);
|
|
87
|
+
program
|
|
88
|
+
.command("compliance")
|
|
89
|
+
.description("assemble a regime-framed evidence pack from the mission (deterministic, read-only; a readiness draft, never a compliance claim)")
|
|
90
|
+
.argument("[regime]", "iso-42001 (nist-ai-rmf / eu-ai-act: coming)")
|
|
91
|
+
.option("-p, --path <path>", "project directory")
|
|
92
|
+
.action(complianceCommand);
|
|
86
93
|
program.parseAsync();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { basename, join, resolve } from "node:path";
|
|
2
|
+
import { findMissionRoot } from "../lib/mission.js";
|
|
3
|
+
import { gatherComplianceInputs, renderIso42001Readiness, renderNistAiRmf, renderEuAiAct, renderOscal } from "../lib/compliance.js";
|
|
4
|
+
import { makeWriter } from "../lib/write.js";
|
|
5
|
+
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
6
|
+
import { VERSION } from "../lib/paths.js";
|
|
7
|
+
/**
|
|
8
|
+
* Assemble a regime-framed compliance evidence pack (ADR-0016) — deterministic, read-only, zero-LLM,
|
|
9
|
+
* outside the gate. Reads the mission's real artifacts (rule→OWASP ASI, conformance manifests, ADR
|
|
10
|
+
* journal, governance docs) and writes an assessment-readiness *draft*, explicitly flagging what only
|
|
11
|
+
* the operator can supply. Never a compliance claim.
|
|
12
|
+
* Exit codes: 0 = draft written, 2 = no mission / unsupported regime.
|
|
13
|
+
*/
|
|
14
|
+
const REGIMES = {
|
|
15
|
+
"iso-42001": { label: "ISO/IEC 42001", render: renderIso42001Readiness, file: "iso-42001-readiness.md" },
|
|
16
|
+
"nist-ai-rmf": { label: "NIST AI RMF", render: renderNistAiRmf, file: "nist-ai-rmf-readiness.md" },
|
|
17
|
+
"eu-ai-act": { label: "EU AI Act (Annex IV)", render: renderEuAiAct, file: "eu-ai-act-readiness.md" },
|
|
18
|
+
};
|
|
19
|
+
export async function complianceCommand(regime, opts) {
|
|
20
|
+
const key = (regime ?? "").toLowerCase();
|
|
21
|
+
console.log(createHeader(`Runward v${VERSION} — compliance`, key ? REGIMES[key]?.label ?? key : "regime required"));
|
|
22
|
+
if (!key || !(key in REGIMES)) {
|
|
23
|
+
console.error(status.error(`Usage: runward compliance <regime>. Supported: ${Object.keys(REGIMES).join(", ")}.`));
|
|
24
|
+
console.log(" " + c.darkGray("The manifest is universal (OWASP ASI); the regime is a lens (ADR-0015). Default posture is security-only — no regime named."));
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
const spec = REGIMES[key];
|
|
28
|
+
if (!spec.render) {
|
|
29
|
+
console.error(status.error(`The ${spec.label} lens is not assembled yet — only iso-42001 for now (ADR-0016, built piece by piece).`));
|
|
30
|
+
console.log(" " + c.darkGray(`See the framing reference: docs/compliance/${key}.md`));
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
|
|
34
|
+
if (!root) {
|
|
35
|
+
console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
const mission = join(root, "runward");
|
|
39
|
+
const dryRun = process.env.RUNWARD_DRY_RUN === "1";
|
|
40
|
+
const generatedAt = isNonInteractive() && process.env.RUNWARD_NOW ? process.env.RUNWARD_NOW : new Date().toISOString().slice(0, 10);
|
|
41
|
+
console.log(section("Assembling (read-only, deterministic)"));
|
|
42
|
+
const inputs = gatherComplianceInputs(mission);
|
|
43
|
+
const md = spec.render(inputs, generatedAt);
|
|
44
|
+
const w = makeWriter({ force: true, dryRun, root }); // generated artifacts — always refresh
|
|
45
|
+
w.write(join(mission, "compliance", spec.file), md);
|
|
46
|
+
// OSCAL export — regime-neutral, machine-readable; the interop layer (ADR-0016) so the evidence flows into GRC/auditor tools.
|
|
47
|
+
w.write(join(mission, "compliance", "oscal-component-definition.json"), renderOscal(inputs, basename(root), generatedAt));
|
|
48
|
+
const mappedAsi = [...inputs.asiCoverage.values()].filter((v) => v.length).length;
|
|
49
|
+
console.log(section("Assembled"));
|
|
50
|
+
console.log(` ${c.primaryBold("ASI coverage")} ${c.white(`${mappedAsi}/10 categories mapped to a rule`)}`);
|
|
51
|
+
console.log(` ${c.primaryBold("Conformance")} ${c.white(`${inputs.conformance.length} accounted rule(s)`)}`);
|
|
52
|
+
console.log(` ${c.primaryBold("Decisions")} ${c.white(`${inputs.adrs.length} ratified ADR(s)`)}`);
|
|
53
|
+
console.log(` ${c.primaryBold("Governance")} ${c.white(`threat model ${inputs.threatModel ? "present" : "missing"}, eval rubric ${inputs.evalRubric ? "present" : "missing"}`)}`);
|
|
54
|
+
console.log(section("Next steps"));
|
|
55
|
+
console.log(" " + c.white("1.") + " Review " + c.primary(`runward/compliance/${spec.file}`) + c.darkGray(" — a draft, not a compliance claim."));
|
|
56
|
+
console.log(" " + c.white("2.") + " Fill the " + c.warning("\"Required from the operator\"") + " sections yourself (applicability, risk acceptance, policy, sign-off).");
|
|
57
|
+
console.log(" " + c.white("3.") + " Machine-readable " + c.primary("runward/compliance/oscal-component-definition.json") + c.darkGray(" (OSCAL) — feed it to your GRC/auditor tool."));
|
|
58
|
+
console.log(" " + c.white("4.") + " Hand it over as " + c.white("supporting evidence") + c.darkGray(" — never as a certification."));
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseManifest } from "./conformance.js";
|
|
5
|
+
import { TEMPLATES } from "./paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* Deterministic, read-only assembly of a regime-framed compliance evidence pack (ADR-0016).
|
|
8
|
+
* It reads the mission's real artifacts at rest — the rule→OWASP ASI mapping, the rule-conformance
|
|
9
|
+
* manifests, the ADR journal, and the governance-doc presence — and assembles an assessment-readiness
|
|
10
|
+
* *draft*. No model call, no live-state scraping, nothing runs. It populates the technical-evidence
|
|
11
|
+
* layer and its index, and explicitly lists what only the operator/organization can supply. It is
|
|
12
|
+
* never a compliance claim.
|
|
13
|
+
*/
|
|
14
|
+
/** OWASP Top 10 for Agentic Applications (ASI01–ASI10), the universal security grammar (ADR-0009). */
|
|
15
|
+
export const ASI_LABELS = {
|
|
16
|
+
ASI01: "Agent Goal Hijack",
|
|
17
|
+
ASI02: "Tool Misuse & Exploitation",
|
|
18
|
+
ASI03: "Identity & Privilege Abuse",
|
|
19
|
+
ASI04: "Agentic Supply Chain Vulnerabilities",
|
|
20
|
+
ASI05: "Unexpected Code Execution",
|
|
21
|
+
ASI06: "Memory & Context Poisoning",
|
|
22
|
+
ASI07: "Insecure Inter-Agent Communication",
|
|
23
|
+
ASI08: "Cascading Failures",
|
|
24
|
+
ASI09: "Human-Agent Trust Exploitation",
|
|
25
|
+
ASI10: "Rogue Agents",
|
|
26
|
+
};
|
|
27
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
|
|
28
|
+
function readRules(missionDir) {
|
|
29
|
+
// The mission's own rules if present (possibly customized), else the package rules — where the
|
|
30
|
+
// shipped OWASP ASI mapping lives (mirrors expectedRules in conformance.ts).
|
|
31
|
+
const missionRules = join(missionDir, "rules");
|
|
32
|
+
const dir = existsSync(missionRules) ? missionRules : join(TEMPLATES, "rules");
|
|
33
|
+
if (!existsSync(dir))
|
|
34
|
+
return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const f of readdirSync(dir)) {
|
|
37
|
+
if (!f.endsWith(".md"))
|
|
38
|
+
continue;
|
|
39
|
+
let fm = "";
|
|
40
|
+
try {
|
|
41
|
+
fm = readFileSync(join(dir, f), "utf8").match(FRONTMATTER)?.[1] ?? "";
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const title = (fm.match(/^title:\s*(.+)$/m)?.[1] ?? f.replace(/\.md$/, "")).trim();
|
|
47
|
+
const impact = (fm.match(/^impact:\s*(.+)$/m)?.[1] ?? "").trim();
|
|
48
|
+
const asiRaw = fm.match(/^asi:\s*\[(.*)\]/m)?.[1] ?? "";
|
|
49
|
+
const asi = asiRaw.split(",").map((s) => s.trim().toUpperCase()).filter((s) => /^ASI\d{2}$/.test(s));
|
|
50
|
+
out.push({ slug: f.replace(/\.md$/, ""), title, impact, asi });
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** The three build-phase deliverables that carry a `## Rule conformance` manifest (as `check --strict`). */
|
|
55
|
+
const CONFORMANCE_DELIVERABLES = [
|
|
56
|
+
["Architect", "architecture.md"],
|
|
57
|
+
["Floor", "floor.md"],
|
|
58
|
+
["Govern", "governance/threat-model.md"],
|
|
59
|
+
];
|
|
60
|
+
function readConformance(missionDir) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const [label, deliverable] of CONFORMANCE_DELIVERABLES) {
|
|
63
|
+
const p = join(missionDir, deliverable);
|
|
64
|
+
if (!existsSync(p))
|
|
65
|
+
continue;
|
|
66
|
+
let body = "";
|
|
67
|
+
try {
|
|
68
|
+
body = readFileSync(p, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const row of parseManifest(body)) {
|
|
74
|
+
if (/^\[.*\]$/.test(row.rule))
|
|
75
|
+
continue; // skip template placeholder rows
|
|
76
|
+
out.push({ ...row, source: label });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function readAdrs(missionDir) {
|
|
82
|
+
const dir = join(missionDir, "adr");
|
|
83
|
+
if (!existsSync(dir))
|
|
84
|
+
return [];
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const f of readdirSync(dir)) {
|
|
87
|
+
if (!f.endsWith(".md") || f === "ADR-0000-template.md" || f.toUpperCase() === "README.MD" || /^DRAFT-/i.test(f))
|
|
88
|
+
continue;
|
|
89
|
+
let body = "";
|
|
90
|
+
try {
|
|
91
|
+
body = readFileSync(join(dir, f), "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const title = (body.match(/^#\s+(.+)$/m)?.[1] ?? f.replace(/\.md$/, "")).trim();
|
|
97
|
+
const status = (body.match(/^\*\*status\*\*\s*:\s*(.+)$/im)?.[1] ?? "").trim();
|
|
98
|
+
out.push({ file: f, title, status });
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
export function gatherComplianceInputs(missionDir) {
|
|
103
|
+
const rules = readRules(missionDir);
|
|
104
|
+
const asiCoverage = new Map();
|
|
105
|
+
for (const id of Object.keys(ASI_LABELS))
|
|
106
|
+
asiCoverage.set(id, []);
|
|
107
|
+
for (const r of rules)
|
|
108
|
+
for (const id of r.asi)
|
|
109
|
+
if (asiCoverage.has(id))
|
|
110
|
+
asiCoverage.get(id).push(r.slug);
|
|
111
|
+
return {
|
|
112
|
+
rules,
|
|
113
|
+
asiCoverage,
|
|
114
|
+
conformance: readConformance(missionDir),
|
|
115
|
+
adrs: readAdrs(missionDir),
|
|
116
|
+
threatModel: existsSync(join(missionDir, "governance", "threat-model.md")),
|
|
117
|
+
evalRubric: existsSync(join(missionDir, "governance", "evaluation-rubric.md")),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Render the ISO/IEC 42001 assessment-readiness draft — the technical-evidence layer + the human-gap list. */
|
|
121
|
+
export function renderIso42001Readiness(inputs, generatedAt) {
|
|
122
|
+
const L = [];
|
|
123
|
+
const counts = { applied: 0, deviated: 0, "n/a": 0 };
|
|
124
|
+
for (const r of inputs.conformance)
|
|
125
|
+
if (counts[r.status] !== undefined)
|
|
126
|
+
counts[r.status]++;
|
|
127
|
+
L.push("# ISO/IEC 42001 — assessment-readiness draft");
|
|
128
|
+
L.push("");
|
|
129
|
+
L.push(`> **Draft, incomplete — not a compliance claim.** Assembled by \`runward compliance iso-42001\` on ${generatedAt},`);
|
|
130
|
+
L.push("> deterministically from ratified engineering artifacts (no model call, nothing scraped or run). It populates the");
|
|
131
|
+
L.push("> **technical-evidence layer and its index**; the applicability, risk-acceptance, policy and management sign-off it");
|
|
132
|
+
L.push("> cannot invent are listed under \"Required from the operator\". This is **supporting evidence**, never certification —");
|
|
133
|
+
L.push("> only an accredited body certifies an AI management system. Verify the current ISO/IEC 42001 text before an audit.");
|
|
134
|
+
L.push("");
|
|
135
|
+
L.push("## 1. Agentic-risk coverage (OWASP ASI → your rules)");
|
|
136
|
+
L.push("");
|
|
137
|
+
L.push("Feeds the ISO 42001 risk assessment (6.1.2) and control selection (6.1.3): which agentic-security risks are addressed by named engineering rules.");
|
|
138
|
+
L.push("");
|
|
139
|
+
L.push("| ASI | Risk | Rules addressing it |");
|
|
140
|
+
L.push("|---|---|---|");
|
|
141
|
+
for (const id of Object.keys(ASI_LABELS)) {
|
|
142
|
+
const slugs = inputs.asiCoverage.get(id) ?? [];
|
|
143
|
+
L.push(`| ${id} | ${ASI_LABELS[id]} | ${slugs.length ? slugs.map((s) => `\`${s}\``).join(", ") : "**no rule mapped — gap to assess**"} |`);
|
|
144
|
+
}
|
|
145
|
+
L.push("");
|
|
146
|
+
L.push("## 2. Control-implementation status (rule conformance)");
|
|
147
|
+
L.push("");
|
|
148
|
+
L.push(`Feeds the Statement of Applicability's implementation-status + evidence columns (6.1.3). From your mission's manifests: **${counts.applied} applied · ${counts.deviated} deviated · ${counts["n/a"]} n/a** across ${inputs.conformance.length} accounted rule(s).`);
|
|
149
|
+
L.push("");
|
|
150
|
+
if (inputs.conformance.length === 0) {
|
|
151
|
+
L.push("_No filled `Rule conformance` manifest found yet — fill the architect/floor/govern deliverables (see `runward check --strict`)._");
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
L.push("| Rule | Status | Evidence | Phase |");
|
|
155
|
+
L.push("|---|---|---|---|");
|
|
156
|
+
for (const r of inputs.conformance)
|
|
157
|
+
L.push(`| \`${r.rule}\` | ${r.status} | ${r.evidence || "—"} | ${r.source} |`);
|
|
158
|
+
}
|
|
159
|
+
L.push("");
|
|
160
|
+
L.push("## 3. Design decisions (ADR journal)");
|
|
161
|
+
L.push("");
|
|
162
|
+
L.push("The \"key design choices, alternatives, and re-evaluation triggers\" an ISO 42001 auditor expects (records under Annex A control groups).");
|
|
163
|
+
L.push("");
|
|
164
|
+
if (inputs.adrs.length === 0) {
|
|
165
|
+
L.push("_No ratified ADR found in `runward/adr/`._");
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
L.push("| ADR | Status |");
|
|
169
|
+
L.push("|---|---|");
|
|
170
|
+
for (const a of inputs.adrs)
|
|
171
|
+
L.push(`| ${a.title} (\`${a.file}\`) | ${a.status || "—"} |`);
|
|
172
|
+
}
|
|
173
|
+
L.push("");
|
|
174
|
+
L.push("## 4. Risk & impact inputs (presence)");
|
|
175
|
+
L.push("");
|
|
176
|
+
L.push(`- Threat model (feeds risk assessment 6.1.2): ${inputs.threatModel ? "**present** — confirm it is filled, not a raw template" : "**missing**"}`);
|
|
177
|
+
L.push(`- Evaluation rubric (feeds impact/validation analysis): ${inputs.evalRubric ? "**present** — confirm it is filled" : "**missing**"}`);
|
|
178
|
+
L.push("");
|
|
179
|
+
L.push("## Required from the operator / organization (runward cannot produce this)");
|
|
180
|
+
L.push("");
|
|
181
|
+
L.push("These sections are managerial, legal or organizational — no tool can assemble them from engineering artifacts:");
|
|
182
|
+
L.push("");
|
|
183
|
+
L.push("- **AI policy** (5.2) and **AIMS scope** (4.3).");
|
|
184
|
+
L.push("- **Statement of Applicability — the applicability decisions and inclusion/exclusion justifications** (6.1.3): runward supplies the status + evidence columns; the *applicability* judgment is yours.");
|
|
185
|
+
L.push("- **Risk methodology, acceptance criteria and risk-acceptance decisions** (6.1.2, 8.3).");
|
|
186
|
+
L.push("- **AI system impact assessment report and deployment authorization** (6.1.4).");
|
|
187
|
+
L.push("- **Objectives and targets** (6.2), **roles and competence** (A.3, 7.2).");
|
|
188
|
+
L.push("- **Internal audit** (9.2) and **management review** minutes (9.3).");
|
|
189
|
+
L.push("- **Runtime AI event logs** (A.6.2.8) — produced by the running system, not by runward.");
|
|
190
|
+
L.push("");
|
|
191
|
+
L.push("_Regime mapping is dated engineering framing, not legal advice; ISO Annex A control counts/templates are behind the paywalled standard — confirm against the purchased text._");
|
|
192
|
+
L.push("");
|
|
193
|
+
return L.join("\n") + "\n";
|
|
194
|
+
}
|
|
195
|
+
// ── Shared table blocks (used by every regime lens) ──
|
|
196
|
+
function asiTableLines(inputs) {
|
|
197
|
+
const L = ["| ASI | Risk | Rules addressing it |", "|---|---|---|"];
|
|
198
|
+
for (const id of Object.keys(ASI_LABELS)) {
|
|
199
|
+
const slugs = inputs.asiCoverage.get(id) ?? [];
|
|
200
|
+
L.push(`| ${id} | ${ASI_LABELS[id]} | ${slugs.length ? slugs.map((s) => `\`${s}\``).join(", ") : "**no rule mapped — gap to assess**"} |`);
|
|
201
|
+
}
|
|
202
|
+
return L;
|
|
203
|
+
}
|
|
204
|
+
function confCounts(inputs) {
|
|
205
|
+
const c = { applied: 0, deviated: 0, "n/a": 0 };
|
|
206
|
+
for (const r of inputs.conformance)
|
|
207
|
+
if (c[r.status] !== undefined)
|
|
208
|
+
c[r.status]++;
|
|
209
|
+
return c;
|
|
210
|
+
}
|
|
211
|
+
function confTableLines(inputs) {
|
|
212
|
+
if (inputs.conformance.length === 0)
|
|
213
|
+
return ["_No filled `Rule conformance` manifest found yet — fill the architect/floor/govern deliverables (`runward check --strict`)._"];
|
|
214
|
+
const L = ["| Rule | Status | Evidence | Phase |", "|---|---|---|---|"];
|
|
215
|
+
for (const r of inputs.conformance)
|
|
216
|
+
L.push(`| \`${r.rule}\` | ${r.status} | ${r.evidence || "—"} | ${r.source} |`);
|
|
217
|
+
return L;
|
|
218
|
+
}
|
|
219
|
+
function adrTableLines(inputs) {
|
|
220
|
+
if (inputs.adrs.length === 0)
|
|
221
|
+
return ["_No ratified ADR found in `runward/adr/`._"];
|
|
222
|
+
const L = ["| ADR | Status |", "|---|---|"];
|
|
223
|
+
for (const a of inputs.adrs)
|
|
224
|
+
L.push(`| ${a.title} (\`${a.file}\`) | ${a.status || "—"} |`);
|
|
225
|
+
return L;
|
|
226
|
+
}
|
|
227
|
+
/** Render the NIST AI RMF assessment-readiness draft — an ASI↔AI-RMF crosswalk, the MEASURE/TEVV
|
|
228
|
+
* documentation, and the design decisions; GOVERN and risk-tolerance stay the operator's. */
|
|
229
|
+
export function renderNistAiRmf(inputs, generatedAt) {
|
|
230
|
+
const c = confCounts(inputs);
|
|
231
|
+
const L = [];
|
|
232
|
+
L.push("# NIST AI RMF — assessment-readiness draft");
|
|
233
|
+
L.push("");
|
|
234
|
+
L.push(`> **Draft, incomplete — not a compliance claim.** Assembled by \`runward compliance nist-ai-rmf\` on ${generatedAt},`);
|
|
235
|
+
L.push("> deterministically from ratified engineering artifacts (no model call). The AI RMF is **voluntary guidance**");
|
|
236
|
+
L.push("> with no pass/fail and no certification; this populates the MEASURE/documentation evidence and an ASI crosswalk,");
|
|
237
|
+
L.push("> while GOVERN, risk tolerance and go/no-go stay the operator's. Verify the current AI RMF text before use.");
|
|
238
|
+
L.push("");
|
|
239
|
+
L.push("## 1. Agentic-risk crosswalk (OWASP ASI → AI RMF)");
|
|
240
|
+
L.push("");
|
|
241
|
+
L.push("An indicative engineering crosswalk (not NIST-endorsed): each agentic-security risk lands primarily under **MEASURE** (test & evaluate, esp. security & resilience) and **MANAGE** (risk treatment). Confirm subcategory selection against AI RMF §5.");
|
|
242
|
+
L.push("");
|
|
243
|
+
L.push(...asiTableLines(inputs));
|
|
244
|
+
L.push("");
|
|
245
|
+
L.push("## 2. MEASURE / TEVV documentation");
|
|
246
|
+
L.push("");
|
|
247
|
+
L.push(`Feeds MEASURE 2.x — documented, repeatable test methodology and results. From your mission: **${c.applied} applied · ${c.deviated} deviated · ${c["n/a"]} n/a** across ${inputs.conformance.length} rule(s).`);
|
|
248
|
+
L.push(`- Evaluation rubric (test sets, metrics, tooling): ${inputs.evalRubric ? "**present** — confirm it is filled" : "**missing**"}`);
|
|
249
|
+
L.push(`- Threat model (adversarial / risk-source analysis): ${inputs.threatModel ? "**present** — confirm it is filled" : "**missing**"}`);
|
|
250
|
+
L.push("");
|
|
251
|
+
L.push(...confTableLines(inputs));
|
|
252
|
+
L.push("");
|
|
253
|
+
L.push("## 3. Design decisions (ADR journal)");
|
|
254
|
+
L.push("");
|
|
255
|
+
L.push(...adrTableLines(inputs));
|
|
256
|
+
L.push("");
|
|
257
|
+
L.push("## Required from the operator / organization (runward cannot produce this)");
|
|
258
|
+
L.push("");
|
|
259
|
+
L.push("- **GOVERN** — policies, roles, accountability, **risk tolerance** (almost entirely organizational).");
|
|
260
|
+
L.push("- **MAP** — intended purpose, business/legal context, use-case risk enumeration.");
|
|
261
|
+
L.push("- **MANAGE** — the **go/no-go acceptance** decision, resourcing, response planning.");
|
|
262
|
+
L.push("- **Profiles** — Current/Target selection, prioritization, the risk-tolerance choices behind them.");
|
|
263
|
+
L.push("");
|
|
264
|
+
L.push("_Indicative engineering framing, not legal advice; NIST prescribes no report template — confirm against AI 100-1 and the Playbook._");
|
|
265
|
+
L.push("");
|
|
266
|
+
return L.join("\n") + "\n";
|
|
267
|
+
}
|
|
268
|
+
/** Render the EU AI Act Annex IV assessment-readiness draft — strong on Point 2 (design/architecture/
|
|
269
|
+
* validation) and the design-rationale/change history (the ADR journal); RMS, standards, declaration
|
|
270
|
+
* and post-market plan stay the provider's. */
|
|
271
|
+
export function renderEuAiAct(inputs, generatedAt) {
|
|
272
|
+
const L = [];
|
|
273
|
+
L.push("# EU AI Act — Annex IV technical documentation — assessment-readiness draft");
|
|
274
|
+
L.push("");
|
|
275
|
+
L.push(`> **Draft, incomplete — not a conformity assessment.** Assembled by \`runward compliance eu-ai-act\` on ${generatedAt},`);
|
|
276
|
+
L.push("> deterministically from ratified engineering artifacts (no model call). High-risk obligations bind from");
|
|
277
|
+
L.push("> **2 August 2026** (Annex III). This populates Annex IV Point 2 (design & validation) and the design-rationale");
|
|
278
|
+
L.push("> history; it does **not** satisfy art. 12 runtime logging, and it is not a signed declaration of conformity.");
|
|
279
|
+
L.push("> Verify against the Official Journal text before filing.");
|
|
280
|
+
L.push("");
|
|
281
|
+
L.push("## Annex IV coverage map");
|
|
282
|
+
L.push("");
|
|
283
|
+
L.push("| Annex IV point | runward supplies | Required from the provider |");
|
|
284
|
+
L.push("|---|---|---|");
|
|
285
|
+
L.push("| 1. General description | UI, HW/SW/firmware notes | intended purpose, provider, versioning, distribution |");
|
|
286
|
+
L.push("| 2. Elements & development | **architecture, validation procedures + metrics, cybersecurity (manifest + rubric + threat model); design choices, alternatives, assumptions, pre-determined changes = the ADR journal** | third-party sourcing/licensing, sign-off on test logs |");
|
|
287
|
+
L.push("| 3. Monitoring & control | accuracy characterization, input-data specs, oversight tooling | fundamental-rights / discrimination risk sourcing |");
|
|
288
|
+
L.push("| 4. Performance metrics | metric-choice justification (rubric) | — |");
|
|
289
|
+
L.push("| 5. Risk management (art. 9) | technical inputs (threat model, testing) | **risk acceptance / RMS governance** |");
|
|
290
|
+
L.push("| 6. Lifecycle changes | engineering change record (ADR journal) | release/change-management governance |");
|
|
291
|
+
L.push("| 7. Harmonised standards | technical notes | **standards selection** (compliance strategy) |");
|
|
292
|
+
L.push("| 8. Declaration of conformity | — | **signed legal act (art. 47)** |");
|
|
293
|
+
L.push("| 9. Post-market monitoring | telemetry/logging backbone | **post-market monitoring plan (art. 72)** |");
|
|
294
|
+
L.push("");
|
|
295
|
+
L.push("## Point 2 — design decisions (ADR journal, near-verbatim to the Annex IV requirement)");
|
|
296
|
+
L.push("");
|
|
297
|
+
L.push(...adrTableLines(inputs));
|
|
298
|
+
L.push("");
|
|
299
|
+
L.push("## Agentic-risk coverage (OWASP ASI → Point 2 cybersecurity / Point 5 risk)");
|
|
300
|
+
L.push("");
|
|
301
|
+
L.push(...asiTableLines(inputs));
|
|
302
|
+
L.push("");
|
|
303
|
+
L.push("## Control-implementation status (feeds Point 2 validation)");
|
|
304
|
+
L.push("");
|
|
305
|
+
L.push(...confTableLines(inputs));
|
|
306
|
+
L.push("");
|
|
307
|
+
L.push("## Required from the provider (runward cannot produce this)");
|
|
308
|
+
L.push("");
|
|
309
|
+
L.push("- **Point 1** general description (intended purpose, provider, versioning) · **Point 5** RMS governance & risk acceptance (art. 9).");
|
|
310
|
+
L.push("- **Point 7** harmonised-standards selection · **Point 8** the signed **EU declaration of conformity** (art. 47) · **Point 9** the **post-market monitoring plan** (art. 72).");
|
|
311
|
+
L.push("- **Art. 12 runtime event logs** — produced by the running system, not by runward.");
|
|
312
|
+
L.push("");
|
|
313
|
+
L.push("_Engineering framing, not legal advice; Annex IV wording moves — confirm against the Official Journal (Reg. (EU) 2024/1689) before filing._");
|
|
314
|
+
L.push("");
|
|
315
|
+
return L.join("\n") + "\n";
|
|
316
|
+
}
|
|
317
|
+
// ── OSCAL export (ADR-0016) — the machine-readable interop layer, so the evidence flows into GRC/auditor tools ──
|
|
318
|
+
/** A deterministic RFC-4122-shaped UUID derived from a stable seed (SHA-256), so two runs on the same
|
|
319
|
+
* artifacts produce byte-identical OSCAL — no random UUIDs to break the determinism invariant. */
|
|
320
|
+
function detUuid(seed) {
|
|
321
|
+
const b = createHash("sha256").update(`runward-oscal:${seed}`).digest("hex").slice(0, 32).split("");
|
|
322
|
+
b[12] = "5"; // version 5 (name-based)
|
|
323
|
+
b[16] = ((parseInt(b[16], 16) & 0x3) | 0x8).toString(16); // RFC-4122 variant
|
|
324
|
+
const s = b.join("");
|
|
325
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
326
|
+
}
|
|
327
|
+
const OSCAL_VERSION = "1.1.2";
|
|
328
|
+
const ASI_CATALOG = "https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/";
|
|
329
|
+
/**
|
|
330
|
+
* Render the OWASP ASI control coverage as an OSCAL component-definition (JSON) — regime-neutral, the
|
|
331
|
+
* universal machine-readable layer. Each ASI category is a control whose implementation-status is
|
|
332
|
+
* derived from the mission's conformance manifest; evidence links back to the readiness draft. It is
|
|
333
|
+
* labelled a DRAFT / supporting evidence in the metadata remarks — never a compliance claim.
|
|
334
|
+
*/
|
|
335
|
+
export function renderOscal(inputs, missionName, generatedAt) {
|
|
336
|
+
const ns = missionName || "mission";
|
|
337
|
+
const irs = Object.keys(ASI_LABELS).map((id) => {
|
|
338
|
+
const slugs = inputs.asiCoverage.get(id) ?? [];
|
|
339
|
+
const statuses = slugs.map((s) => inputs.conformance.find((c) => c.rule === s)?.status).filter(Boolean);
|
|
340
|
+
let impl;
|
|
341
|
+
if (slugs.length === 0)
|
|
342
|
+
impl = "planned"; // no rule maps this risk — a gap
|
|
343
|
+
else if (statuses.length > 0 && statuses.every((s) => s === "applied"))
|
|
344
|
+
impl = "implemented";
|
|
345
|
+
else
|
|
346
|
+
impl = "partial"; // mapped, but deviated / n-a / not yet in a manifest
|
|
347
|
+
return {
|
|
348
|
+
uuid: detUuid(`${ns}:ir:${id}`),
|
|
349
|
+
"control-id": `asi-${id.slice(3)}`,
|
|
350
|
+
description: `${id} ${ASI_LABELS[id]}. ${slugs.length ? "Addressed by rules: " + slugs.join(", ") + "." : "No rule mapped — gap to assess."}`,
|
|
351
|
+
props: [{ name: "implementation-status", value: impl }],
|
|
352
|
+
links: [{ href: "./iso-42001-readiness.md", rel: "reference", text: "runward assessment-readiness draft" }],
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
const doc = {
|
|
356
|
+
"component-definition": {
|
|
357
|
+
uuid: detUuid(`${ns}:component-definition`),
|
|
358
|
+
metadata: {
|
|
359
|
+
title: `runward — agentic-security control evidence (${missionName})`,
|
|
360
|
+
"last-modified": `${generatedAt}T00:00:00Z`,
|
|
361
|
+
version: generatedAt,
|
|
362
|
+
"oscal-version": OSCAL_VERSION,
|
|
363
|
+
remarks: "Assessment-readiness DRAFT, assembled deterministically by runward from ratified engineering artifacts (rule to OWASP ASI mapping, conformance manifest, ADR journal). Supporting evidence only — NOT a compliance claim, NOT a certification, NOT a conformity assessment. Applicability, risk acceptance and management sign-off are the operator's.",
|
|
364
|
+
},
|
|
365
|
+
components: [{
|
|
366
|
+
uuid: detUuid(`${ns}:component`),
|
|
367
|
+
type: "software",
|
|
368
|
+
title: missionName,
|
|
369
|
+
description: "The agentic system governed by this runward mission.",
|
|
370
|
+
"control-implementations": [{
|
|
371
|
+
uuid: detUuid(`${ns}:control-implementation`),
|
|
372
|
+
source: ASI_CATALOG,
|
|
373
|
+
description: "OWASP Top 10 for Agentic Applications (ASI01–ASI10) coverage, derived from the mission's craft-rule mapping and conformance manifest.",
|
|
374
|
+
"implemented-requirements": irs,
|
|
375
|
+
}],
|
|
376
|
+
}],
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
380
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runward",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "After the spec: ship and run. A delivery framework for agentic systems — floor first, evolution on evidence, governance from day zero, handover with proof.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic",
|