nexarch 0.13.5 → 0.13.7
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.
|
@@ -507,6 +507,36 @@ export function instructionInjectionDetail(registrationOk, staleAttestationTarge
|
|
|
507
507
|
return `updated ${agentConfigResults.length} instruction target file(s)`;
|
|
508
508
|
return "no runtime instruction target matched this repository (non-fatal; create AGENTS.md/CLAUDE.md or configure a generic target)";
|
|
509
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Summarises the per-target attestation writes into the check's ok/detail.
|
|
512
|
+
*
|
|
513
|
+
* This used to be `trustAttestation?.ok`, which was the *first* target's mint
|
|
514
|
+
* result, reported under plural wording ("instruction file(s)") as if it
|
|
515
|
+
* covered them all. On Paul's 20 September setup run that printed a green
|
|
516
|
+
* "minted and injected into instruction file(s)" while only AGENTS.md was
|
|
517
|
+
* refreshed: CLAUDE.md and .cursorrules kept an attestation that had expired
|
|
518
|
+
* 22 minutes earlier, under the pre-ADR-0112 `instruction_injection` scope.
|
|
519
|
+
* The two agents that then read CLAUDE.md cold reported it as an injection
|
|
520
|
+
* attempt, which is the correct reading of what was in front of them.
|
|
521
|
+
*
|
|
522
|
+
* Green here now requires every target to have been verified on disk after
|
|
523
|
+
* writing, and a failure names the files rather than hiding behind a count.
|
|
524
|
+
*/
|
|
525
|
+
export function summariseAttestationOutcomes(outcomes) {
|
|
526
|
+
if (outcomes.length === 0)
|
|
527
|
+
return { ok: true, detail: "" };
|
|
528
|
+
const failures = outcomes.filter((outcome) => !outcome.ok);
|
|
529
|
+
if (failures.length === 0) {
|
|
530
|
+
return { ok: true, detail: `minted and injected into ${outcomes.length} instruction file(s)` };
|
|
531
|
+
}
|
|
532
|
+
const named = failures
|
|
533
|
+
.map((failure) => `${basename(failure.path)} (${failure.reason ?? "unknown"})`)
|
|
534
|
+
.join(", ");
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
detail: `${outcomes.length - failures.length}/${outcomes.length} instruction file(s) attested; failed: ${named}`,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
510
540
|
export function buildTrustAttestationSection(instructionFile, attestation) {
|
|
511
541
|
if (!attestation.token || !attestation.payload)
|
|
512
542
|
return null;
|
|
@@ -1292,6 +1322,7 @@ export async function initAgent(args) {
|
|
|
1292
1322
|
let unsafeReattestationTargetCount = 0;
|
|
1293
1323
|
let trustAttestation = null;
|
|
1294
1324
|
let trustAttestationAttempted = false;
|
|
1325
|
+
const attestationOutcomes = [];
|
|
1295
1326
|
if (registration.ok) {
|
|
1296
1327
|
try {
|
|
1297
1328
|
// Save identity so check-in can find the agent key
|
|
@@ -1385,7 +1416,9 @@ export async function initAgent(args) {
|
|
|
1385
1416
|
// section can carry different text (different templateCode per
|
|
1386
1417
|
// runtime), and the attestation has to bind to the exact bytes it
|
|
1387
1418
|
// covers — see ADR-0112. `trustAttestation` keeps the first result for
|
|
1388
|
-
// the JSON summary field; every target still gets its own token
|
|
1419
|
+
// the JSON summary field; every target still gets its own token, and
|
|
1420
|
+
// every target's write is recorded and verified independently so one
|
|
1421
|
+
// success can no longer stand in for the batch.
|
|
1389
1422
|
for (const r of attestationTargets) {
|
|
1390
1423
|
let targetAttestation;
|
|
1391
1424
|
try {
|
|
@@ -1397,18 +1430,38 @@ export async function initAgent(args) {
|
|
|
1397
1430
|
}
|
|
1398
1431
|
if (!trustAttestation)
|
|
1399
1432
|
trustAttestation = targetAttestation;
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1433
|
+
if (!targetAttestation.ok) {
|
|
1434
|
+
const reason = targetAttestation.reason ?? "unknown";
|
|
1435
|
+
try {
|
|
1436
|
+
injectTrustAttestationUnavailableBlock(r.path, reason);
|
|
1403
1437
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1438
|
+
catch {
|
|
1439
|
+
// Best-effort notice only; the recorded outcome below is what the check reports.
|
|
1406
1440
|
}
|
|
1441
|
+
attestationOutcomes.push({ path: r.path, ok: false, reason: `mint failed (${reason})` });
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
injectTrustAttestationBlock(r.path, targetAttestation);
|
|
1407
1446
|
injectInitProjectReportingContract(r.path);
|
|
1408
1447
|
}
|
|
1409
|
-
catch {
|
|
1410
|
-
// non-fatal
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
// Previously swallowed as "non-fatal", which is how a failed write
|
|
1450
|
+
// reported as a minted-and-injected success.
|
|
1451
|
+
attestationOutcomes.push({
|
|
1452
|
+
path: r.path,
|
|
1453
|
+
ok: false,
|
|
1454
|
+
reason: `write failed (${error instanceof Error ? error.message : "unknown"})`,
|
|
1455
|
+
});
|
|
1456
|
+
continue;
|
|
1411
1457
|
}
|
|
1458
|
+
// Returning without throwing is not proof the block landed: a replace
|
|
1459
|
+
// that matched nothing writes the file unchanged. Re-read and confirm
|
|
1460
|
+
// the file now carries a current, correctly-scoped attestation.
|
|
1461
|
+
const staleness = trustAttestationStaleness(r.path);
|
|
1462
|
+
attestationOutcomes.push(staleness
|
|
1463
|
+
? { path: r.path, ok: false, reason: `write did not take (${staleness})` }
|
|
1464
|
+
: { path: r.path, ok: true });
|
|
1412
1465
|
}
|
|
1413
1466
|
}
|
|
1414
1467
|
}
|
|
@@ -1428,14 +1481,18 @@ export async function initAgent(args) {
|
|
|
1428
1481
|
: identityCapture.detail,
|
|
1429
1482
|
});
|
|
1430
1483
|
const instructionsAlreadyConfigured = agentConfigResults.length > 0 && agentConfigResults.every((r) => r.status === "already_present");
|
|
1484
|
+
const attestationSummary = summariseAttestationOutcomes(attestationOutcomes);
|
|
1485
|
+
/** Renewed means every attested target verified on disk, not that one mint succeeded. */
|
|
1486
|
+
const attestationRenewed = attestationOutcomes.length > 0 && attestationSummary.ok;
|
|
1431
1487
|
checks.push({
|
|
1432
1488
|
name: "agent.instructions.injection",
|
|
1433
1489
|
ok: !registration.ok || unsafeReattestationTargetCount === 0 && (!instructionsWriteAllowed || agentConfigResults.length > 0),
|
|
1434
|
-
detail: instructionInjectionDetail(registration.ok, staleAttestationTargetCount, mismatchedAttestationTargetCount, unsafeReattestationTargetCount, instructionsWriteAllowed,
|
|
1490
|
+
detail: instructionInjectionDetail(registration.ok, staleAttestationTargetCount, mismatchedAttestationTargetCount, unsafeReattestationTargetCount, instructionsWriteAllowed, attestationRenewed, instructionsAlreadyConfigured, agentConfigResults),
|
|
1435
1491
|
});
|
|
1436
1492
|
checks.push({
|
|
1437
1493
|
name: "agent.trust.attestation",
|
|
1438
1494
|
ok: !registration.ok || (unsafeReattestationTargetCount === 0
|
|
1495
|
+
&& attestationSummary.ok
|
|
1439
1496
|
&& ((staleAttestationTargetCount === 0 && !trustAttestationAttempted) || Boolean(trustAttestation?.ok))),
|
|
1440
1497
|
detail: !registration.ok
|
|
1441
1498
|
? "skipped (registration failed)"
|
|
@@ -1451,9 +1508,7 @@ export async function initAgent(args) {
|
|
|
1451
1508
|
? agentConfigResults.length > 0
|
|
1452
1509
|
? "already current (no refresh needed)"
|
|
1453
1510
|
: "skipped (no instruction target written)"
|
|
1454
|
-
:
|
|
1455
|
-
? "minted and injected into instruction file(s)"
|
|
1456
|
-
: `unavailable (${trustAttestation?.reason ?? "unknown"})`,
|
|
1511
|
+
: attestationSummary.detail,
|
|
1457
1512
|
});
|
|
1458
1513
|
checks.push({
|
|
1459
1514
|
name: "technology.components",
|
|
@@ -1537,6 +1592,10 @@ export async function initAgent(args) {
|
|
|
1537
1592
|
}
|
|
1538
1593
|
: null,
|
|
1539
1594
|
trustAttestation,
|
|
1595
|
+
// Per-target truth. `trustAttestation` above is only the first target's
|
|
1596
|
+
// mint result, kept for back-compat; this is what says whether each
|
|
1597
|
+
// instruction file actually ends up carrying a current attestation.
|
|
1598
|
+
trustAttestationTargets: attestationOutcomes,
|
|
1540
1599
|
companyId: creds.companyId,
|
|
1541
1600
|
registry: { version: registry.release.version, registryVersion: registry.registryVersion, publishedAt: registry.release.publishedAt },
|
|
1542
1601
|
agentConfigs: agentConfigResults,
|
|
@@ -91,13 +91,20 @@ Options:
|
|
|
91
91
|
console.log(` State: ${run.auditState} — required rules ${requiredPct} pass` +
|
|
92
92
|
(required ? ` (${required.pass ?? 0} of ${required.total ?? 0}, ${required.partial ?? 0} partial, ${required.fail ?? 0} fail)` : "") +
|
|
93
93
|
`; advisory findings: ${run.advisoryIssueCount ?? 0} (do not affect state)`);
|
|
94
|
+
if ((run.criticalIssueCount ?? 0) > 0 || (run.highIssueCount ?? 0) > 0) {
|
|
95
|
+
console.log(` Severity: ${run.criticalIssueCount ?? 0} critical, ${run.highIssueCount ?? 0} high open finding(s)`);
|
|
96
|
+
}
|
|
97
|
+
if ((run.unscoredSeverityCount ?? 0) > 0) {
|
|
98
|
+
console.log(` Unrated: ${run.unscoredSeverityCount} finding(s) have no severity assigned`);
|
|
99
|
+
}
|
|
94
100
|
}
|
|
95
101
|
for (const control of run.controls ?? []) {
|
|
96
102
|
console.log(`\n - ${control.controlName} (${control.controlId})`);
|
|
97
103
|
for (const rule of control.rules ?? []) {
|
|
98
104
|
const qualifier = rule.requirementQualifier ? ` ${rule.requirementQualifier}` : "";
|
|
99
105
|
const level = rule.requirementLevel ? ` [${rule.requirementLevel}${qualifier}]` : "";
|
|
100
|
-
|
|
106
|
+
const ranking = [rule.severity, rule.effort, rule.exploitability].filter(Boolean).join(", ");
|
|
107
|
+
console.log(` • [${rule.result}] ${rule.ruleName}${level} (${rule.ruleId})${ranking ? ` {${ranking}}` : ""}`);
|
|
101
108
|
if (rule.rationale)
|
|
102
109
|
console.log(` ${rule.rationale}`);
|
|
103
110
|
if (Array.isArray(rule.missingRequirements) && rule.missingRequirements.length > 0) {
|
|
@@ -105,6 +112,12 @@ Options:
|
|
|
105
112
|
console.log(` - missing: ${String(m)}`);
|
|
106
113
|
}
|
|
107
114
|
}
|
|
115
|
+
if (Array.isArray(rule.fileRefs) && rule.fileRefs.length > 0) {
|
|
116
|
+
for (const ref of rule.fileRefs) {
|
|
117
|
+
const loc = ref.line !== undefined && ref.line !== null ? `${ref.path}:${ref.line}` : ref.path;
|
|
118
|
+
console.log(` - ${loc}${ref.note ? ` — ${ref.note}` : ""}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
123
|
}
|
|
@@ -44,21 +44,36 @@ function loadIdentityAgentKey() {
|
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function isSeverity(value) {
|
|
48
|
+
return value === "low" || value === "medium" || value === "high" || value === "critical";
|
|
49
|
+
}
|
|
50
|
+
function isEffort(value) {
|
|
51
|
+
return value === "hours" || value === "days" || value === "weeks";
|
|
52
|
+
}
|
|
53
|
+
function isExploitability(value) {
|
|
54
|
+
return value === "theoretical" || value === "plausible" || value === "confirmed";
|
|
55
|
+
}
|
|
47
56
|
function parseFindingToken(token) {
|
|
48
|
-
const [policyControlId, policyRuleId, resultRaw, rationaleRaw, missingRaw] = token.split("|");
|
|
57
|
+
const [policyControlId, policyRuleId, resultRaw, rationaleRaw, missingRaw, severityRaw] = token.split("|");
|
|
49
58
|
const result = (resultRaw ?? "").trim().toLowerCase();
|
|
50
59
|
if (!policyControlId?.trim() || !policyRuleId?.trim() || !result) {
|
|
51
|
-
throw new Error("Each --finding must be: <policyControlId>|<policyRuleId>|<pass|partial|fail>|<rationale>|<missing1;missing2>");
|
|
60
|
+
throw new Error("Each --finding must be: <policyControlId>|<policyRuleId>|<pass|partial|fail>|<rationale>|<missing1;missing2>|<severity>");
|
|
52
61
|
}
|
|
53
62
|
if (result !== "pass" && result !== "partial" && result !== "fail") {
|
|
54
63
|
throw new Error(`Invalid finding result '${resultRaw}'. Use pass|partial|fail.`);
|
|
55
64
|
}
|
|
65
|
+
const severityInput = severityRaw?.trim().toLowerCase();
|
|
66
|
+
if (severityInput && !isSeverity(severityInput)) {
|
|
67
|
+
throw new Error(`Invalid severity '${severityRaw}'. Use low|medium|high|critical.`);
|
|
68
|
+
}
|
|
69
|
+
const severity = severityInput && isSeverity(severityInput) ? severityInput : undefined;
|
|
56
70
|
return {
|
|
57
71
|
policyControlId: policyControlId.trim(),
|
|
58
72
|
policyRuleId: policyRuleId.trim(),
|
|
59
73
|
result,
|
|
60
74
|
...(rationaleRaw?.trim() ? { rationale: rationaleRaw.trim() } : {}),
|
|
61
75
|
...(missingRaw?.trim() ? { missingRequirements: missingRaw.split(";").map((m) => m.trim()).filter(Boolean) } : {}),
|
|
76
|
+
...(severity ? { severity } : {}),
|
|
62
77
|
};
|
|
63
78
|
}
|
|
64
79
|
function parseFindings(args) {
|
|
@@ -100,12 +115,26 @@ function parseFindings(args) {
|
|
|
100
115
|
: Array.isArray(value.missing_requirements)
|
|
101
116
|
? value.missing_requirements.map(String)
|
|
102
117
|
: undefined;
|
|
118
|
+
const severity = isSeverity(value.severity) ? value.severity : undefined;
|
|
119
|
+
const effort = isEffort(value.effort) ? value.effort : undefined;
|
|
120
|
+
const exploitability = isExploitability(value.exploitability) ? value.exploitability : undefined;
|
|
121
|
+
const confidence = typeof value.confidence === "number" ? value.confidence : undefined;
|
|
122
|
+
const fileRefs = Array.isArray(value.fileRefs)
|
|
123
|
+
? value.fileRefs
|
|
124
|
+
: Array.isArray(value.file_refs)
|
|
125
|
+
? value.file_refs
|
|
126
|
+
: undefined;
|
|
103
127
|
return {
|
|
104
128
|
policyControlId,
|
|
105
129
|
policyRuleId,
|
|
106
130
|
result,
|
|
107
131
|
...(rationale ? { rationale } : {}),
|
|
108
132
|
...(missingRequirements ? { missingRequirements } : {}),
|
|
133
|
+
...(severity ? { severity } : {}),
|
|
134
|
+
...(effort ? { effort } : {}),
|
|
135
|
+
...(exploitability ? { exploitability } : {}),
|
|
136
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
137
|
+
...(fileRefs ? { fileRefs } : {}),
|
|
109
138
|
};
|
|
110
139
|
});
|
|
111
140
|
}
|
|
@@ -126,7 +155,7 @@ Options:
|
|
|
126
155
|
--command-id <id> Required command id
|
|
127
156
|
--application-ref <key> Required application reference key (e.g. application:bad-driving)
|
|
128
157
|
--agent-key <key> Optional agent key (defaults from identity)
|
|
129
|
-
--finding <controlId|ruleId|result|rationale|missing1;missing2> Repeatable
|
|
158
|
+
--finding <controlId|ruleId|result|rationale|missing1;missing2|severity> Repeatable
|
|
130
159
|
--findings-json <json> JSON array of findings
|
|
131
160
|
--findings-file <path> Path to JSON array of findings
|
|
132
161
|
--json Print JSON response
|
|
@@ -135,6 +164,12 @@ Notes:
|
|
|
135
164
|
- Findings are rule-level (policyRuleId is required).
|
|
136
165
|
- You can submit partial findings multiple times for the same command.
|
|
137
166
|
- Get valid rule ids with: nexarch policy-controls --entity <application:key> --json
|
|
167
|
+
- Rate severity (low|medium|high|critical), effort (hours|days|weeks) and
|
|
168
|
+
exploitability (theoretical|plausible|confirmed) per finding where you can —
|
|
169
|
+
these decide how findings get ranked. --finding's severity is its 6th field;
|
|
170
|
+
--findings-json/--findings-file also accept effort, exploitability,
|
|
171
|
+
confidence (0-1) and fileRefs ([{path, line, note}]) per finding.
|
|
172
|
+
Omit rather than guess.
|
|
138
173
|
`);
|
|
139
174
|
return;
|
|
140
175
|
}
|
|
@@ -180,6 +215,12 @@ Notes:
|
|
|
180
215
|
console.log(`Required rules: ${pct} pass, ${result.summary.requiredIssueCount} issue(s) — these decide the audit state`);
|
|
181
216
|
console.log(`Advisory findings: ${result.summary.advisoryIssueCount ?? 0} (recommended/informational; recorded, not gating)`);
|
|
182
217
|
}
|
|
218
|
+
if ((result.summary.criticalIssueCount ?? 0) > 0 || (result.summary.highIssueCount ?? 0) > 0) {
|
|
219
|
+
console.log(`Severity: ${result.summary.criticalIssueCount ?? 0} critical, ${result.summary.highIssueCount ?? 0} high (open findings rated by the submitting agent)`);
|
|
220
|
+
}
|
|
221
|
+
if ((result.summary.unscoredSeverityCount ?? 0) > 0) {
|
|
222
|
+
console.log(`Unrated for severity: ${result.summary.unscoredSeverityCount} finding(s) — consider adding severity so this run can be ranked`);
|
|
223
|
+
}
|
|
183
224
|
if ((result.summary.remainingRules ?? 0) > 0) {
|
|
184
225
|
console.log(`Remaining rules to submit: ${result.summary.remainingRules}`);
|
|
185
226
|
}
|
|
@@ -50,6 +50,11 @@ Options:
|
|
|
50
50
|
|
|
51
51
|
Output shape is ready for:
|
|
52
52
|
nexarch policy-audit-submit --findings-file <path.json>
|
|
53
|
+
|
|
54
|
+
Each entry includes empty severity/effort/exploitability/confidence/fileRefs
|
|
55
|
+
placeholders. Fill them in for every partial/fail finding -- they decide how
|
|
56
|
+
findings get ranked and reported. Leave a field blank/empty rather than
|
|
57
|
+
guess; policy-audit-submit drops anything that isn't a valid value.
|
|
53
58
|
`);
|
|
54
59
|
return;
|
|
55
60
|
}
|
|
@@ -76,6 +81,13 @@ Output shape is ready for:
|
|
|
76
81
|
result: defaultResult,
|
|
77
82
|
rationale: "",
|
|
78
83
|
missingRequirements: [],
|
|
84
|
+
// Fill in for partial/fail findings -- see --help. Left blank rather
|
|
85
|
+
// than defaulted, so an unrated finding stays honestly unrated.
|
|
86
|
+
severity: "",
|
|
87
|
+
effort: "",
|
|
88
|
+
exploitability: "",
|
|
89
|
+
confidence: null,
|
|
90
|
+
fileRefs: [],
|
|
79
91
|
})));
|
|
80
92
|
if (findings.length === 0) {
|
|
81
93
|
console.error("error: no rules found for selected controls/entity");
|