filegrc 0.3.3 → 0.4.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 +17 -7
- package/model/index.js +37 -3
- package/model/v1.json +89 -52
- package/model/v2.json +8022 -0
- package/package.json +1 -1
- package/src/agent.js +36 -8
- package/src/audit-preparation.js +63 -60
- package/src/cli.js +176 -113
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +115 -75
- package/src/files.js +230 -28
- package/src/git-name.js +16 -0
- package/src/git.js +702 -6
- package/src/index.js +9 -6
- package/src/model-docs.js +88 -7
- package/src/model-migration.js +1463 -0
- package/src/mutation.js +46 -1
- package/src/obligations.js +108 -84
- package/src/parties.js +17 -2
- package/src/program-path.js +31 -58
- package/src/program-readiness.js +142 -106
- package/src/resource-status.js +17 -0
- package/src/server.js +175 -36
- package/src/setup.js +27 -28
- package/src/state.js +93 -27
- package/src/timing.js +41 -0
- package/src/validate.js +611 -43
- package/src/web.js +586 -141
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/src/cli.js
CHANGED
|
@@ -6,7 +6,6 @@ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldRes
|
|
|
6
6
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
7
7
|
import { buildWorkspace } from "./build.js";
|
|
8
8
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
9
|
-
import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
|
|
10
9
|
import {
|
|
11
10
|
addEvidenceAttachment,
|
|
12
11
|
createResource,
|
|
@@ -15,6 +14,8 @@ import {
|
|
|
15
14
|
updateResource
|
|
16
15
|
} from "./files.js";
|
|
17
16
|
import { generateModelDocumentation } from "./model-docs.js";
|
|
17
|
+
import { migrateModel, planModelMigration } from "./model-migration.js";
|
|
18
|
+
import { normalizeResourceMutation } from "./mutation.js";
|
|
18
19
|
import {
|
|
19
20
|
completeObligationAction,
|
|
20
21
|
completeObligationEvent,
|
|
@@ -23,9 +24,10 @@ import {
|
|
|
23
24
|
planObligations
|
|
24
25
|
} from "./obligations.js";
|
|
25
26
|
import { relativeToWorkspace, resolveDataPath } from "./paths.js";
|
|
26
|
-
import { buildAgentProgramPath
|
|
27
|
-
import { assessProgramReadiness } from "./program-readiness.js";
|
|
27
|
+
import { buildAgentProgramPath } from "./program-path.js";
|
|
28
|
+
import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
28
29
|
import { markdownEntries } from "./resource-markdown.js";
|
|
30
|
+
import { effectiveResourceStatus } from "./resource-status.js";
|
|
29
31
|
import { searchResources } from "./search.js";
|
|
30
32
|
import { serveWorkspace } from "./server.js";
|
|
31
33
|
import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
@@ -36,6 +38,7 @@ import { validateWorkspace } from "./validate.js";
|
|
|
36
38
|
import { loadWorkspace } from "./workspace.js";
|
|
37
39
|
|
|
38
40
|
const BOOLEAN_FLAGS = new Set([
|
|
41
|
+
"allow-non-authoritative-writes",
|
|
39
42
|
"check-docs",
|
|
40
43
|
"complete",
|
|
41
44
|
"current",
|
|
@@ -63,7 +66,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
63
66
|
if (command === "serve") {
|
|
64
67
|
const result = await serveWorkspace(positionals[0] ?? root, {
|
|
65
68
|
host: flags.host ?? process.env.FILEGRC_HOST,
|
|
66
|
-
port: flags.port ?? process.env.FILEGRC_PORT
|
|
69
|
+
port: flags.port ?? process.env.FILEGRC_PORT,
|
|
70
|
+
allowNonAuthoritativeWrites: flags["allow-non-authoritative-writes"] === true
|
|
67
71
|
});
|
|
68
72
|
const stopped = new Promise((resolvePromise) => {
|
|
69
73
|
const stop = () => {
|
|
@@ -87,7 +91,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
87
91
|
...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
|
|
88
92
|
...(flags.owner !== undefined ? { ownerId: flags.owner } : {}),
|
|
89
93
|
...(flags.criticality !== undefined ? { criticality: flags.criticality } : {}),
|
|
90
|
-
...(flags.classification !== undefined ? {
|
|
94
|
+
...(flags.classification !== undefined ? { classificationId: flags.classification } : {}),
|
|
91
95
|
...(flags["internet-exposed"] !== undefined ? { internetExposed: flags["internet-exposed"] } : {}),
|
|
92
96
|
...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
|
|
93
97
|
...(flags.draft ? { draft: true } : {})
|
|
@@ -99,7 +103,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
99
103
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
100
104
|
else if (flags.preview) {
|
|
101
105
|
console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
|
|
102
|
-
console.log("No controls will be linked and no evidence
|
|
106
|
+
console.log("No controls will be linked and no evidence records will be created.");
|
|
103
107
|
}
|
|
104
108
|
else {
|
|
105
109
|
console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
|
|
@@ -142,6 +146,35 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
142
146
|
else console.log(source);
|
|
143
147
|
return;
|
|
144
148
|
}
|
|
149
|
+
if (command === "migrate") {
|
|
150
|
+
const targetModel = String(flags["to-model"] || "");
|
|
151
|
+
if (targetModel !== "2") throw new Error("Pass --to-model 2.");
|
|
152
|
+
const options = {
|
|
153
|
+
jobTitle: flags["job-title"],
|
|
154
|
+
startsOn: flags["starts-on"]
|
|
155
|
+
};
|
|
156
|
+
const plan = await planModelMigration(root, options);
|
|
157
|
+
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
158
|
+
throw new Error("Review migrate --to-model 2 --preview --json, then pass --yes to apply the migration.");
|
|
159
|
+
}
|
|
160
|
+
const result = flags.preview
|
|
161
|
+
? plan
|
|
162
|
+
: plan.sourceModelVersion !== plan.targetModelVersion
|
|
163
|
+
? await migrateModel(root, options)
|
|
164
|
+
: { ...plan, applied: false };
|
|
165
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
166
|
+
else if (result.sourceModelVersion === result.targetModelVersion) {
|
|
167
|
+
console.log(`Workspace already uses model v${result.targetModelVersion}.`);
|
|
168
|
+
} else if (flags.preview) {
|
|
169
|
+
console.log(`Model migration preview: create ${result.summary.create}; update ${result.summary.update}.`);
|
|
170
|
+
console.log(result.ready
|
|
171
|
+
? "Ready to apply. Rerun with --yes."
|
|
172
|
+
: `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
145
178
|
if (command === "describe") {
|
|
146
179
|
const loaded = await loadWorkspace(root);
|
|
147
180
|
const type = positionals[0];
|
|
@@ -193,11 +226,18 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
193
226
|
const loaded = await loadWorkspace(root);
|
|
194
227
|
const type = positionals[0];
|
|
195
228
|
if (type && !loaded.model.resources[type]) throw new Error(`Unknown resource type "${type}".`);
|
|
229
|
+
const asOf = currentCalendarDate(loaded.workspace.timezone);
|
|
196
230
|
const records = loaded.resources
|
|
197
231
|
.filter((record) => !type || record.type === type)
|
|
198
|
-
.sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
|
|
232
|
+
.sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
|
|
233
|
+
.map((record) => {
|
|
234
|
+
const effectiveStatus = effectiveResourceStatus(record, asOf);
|
|
235
|
+
return effectiveStatus && effectiveStatus !== record.status
|
|
236
|
+
? { ...record, effectiveStatus }
|
|
237
|
+
: record;
|
|
238
|
+
});
|
|
199
239
|
if (flags.json) console.log(JSON.stringify(records, null, 2));
|
|
200
|
-
else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.status ?? ""}\t${record.title}`);
|
|
240
|
+
else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.effectiveStatus ?? record.status ?? ""}\t${record.title}`);
|
|
201
241
|
return records;
|
|
202
242
|
}
|
|
203
243
|
if (command === "search") {
|
|
@@ -215,7 +255,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
215
255
|
from: flags.from,
|
|
216
256
|
through: flags.through,
|
|
217
257
|
now: flags.now,
|
|
218
|
-
includeComplete: Boolean(flags.complete)
|
|
258
|
+
includeComplete: Boolean(flags.complete),
|
|
259
|
+
model: loaded.model
|
|
219
260
|
});
|
|
220
261
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
221
262
|
else {
|
|
@@ -234,7 +275,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
234
275
|
if (result.triggers.length) {
|
|
235
276
|
console.log("\nPolicy Events:");
|
|
236
277
|
for (const trigger of result.triggers) {
|
|
237
|
-
console.log(`${trigger.programStatus.toUpperCase()}\t${
|
|
278
|
+
console.log(`${trigger.programStatus.toUpperCase()}\t${trigger.title} (${trigger.eventType})\t${trigger.steps.length} Work Queue ${trigger.steps.length === 1 ? "task" : "tasks"}`);
|
|
238
279
|
for (const step of trigger.steps) {
|
|
239
280
|
const owners = step.ownerIds.length ? step.ownerIds.join(",") : "unassigned";
|
|
240
281
|
const proof = step.completionResourceTypes.length ? step.completionResourceTypes.join("|") : "not specified";
|
|
@@ -260,7 +301,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
260
301
|
}
|
|
261
302
|
else {
|
|
262
303
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
|
|
263
|
-
console.log(
|
|
304
|
+
console.log(
|
|
305
|
+
`${result.target.label}`
|
|
306
|
+
+ (result.target.candidateCoverage?.kind === "range"
|
|
307
|
+
? `, candidate period starts ${result.target.candidateCoverage.startsOn}`
|
|
308
|
+
: result.target.candidateCoverage?.kind === "as-of"
|
|
309
|
+
? `, candidate as-of date ${result.target.candidateCoverage.on}`
|
|
310
|
+
: "")
|
|
311
|
+
);
|
|
264
312
|
for (const stage of result.stages) {
|
|
265
313
|
console.log(`\n${stage.title}`);
|
|
266
314
|
for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
@@ -272,33 +320,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
272
320
|
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
273
321
|
return output;
|
|
274
322
|
}
|
|
275
|
-
if (command === "evidence-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const plan = planEvidenceTestDrafts(loaded);
|
|
279
|
-
const result = {
|
|
280
|
-
schemaVersion: 1,
|
|
281
|
-
preview: true,
|
|
282
|
-
total: plan.length,
|
|
283
|
-
create: plan.filter(({ existing }) => !existing).map((item) => ({
|
|
284
|
-
familyId: item.familyId,
|
|
285
|
-
title: item.title,
|
|
286
|
-
testEvidenceKind: item.testEvidenceKind,
|
|
287
|
-
controlIds: item.controlIds
|
|
288
|
-
})),
|
|
289
|
-
existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
|
|
290
|
-
id: existing.id,
|
|
291
|
-
title: existing.title,
|
|
292
|
-
status: existing.status
|
|
293
|
-
}))
|
|
294
|
-
};
|
|
295
|
-
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
296
|
-
else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
const result = await ensureEvidenceTestDrafts(root);
|
|
323
|
+
if (command === "evidence-map") {
|
|
324
|
+
const loaded = await loadWorkspace(root);
|
|
325
|
+
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"] });
|
|
300
326
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
301
|
-
else
|
|
327
|
+
else {
|
|
328
|
+
console.log(`${result.status.toUpperCase()}: ${result.counts.complete} mapped, ${result.counts.action} need action`);
|
|
329
|
+
for (const item of result.items) {
|
|
330
|
+
console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
331
|
+
if (item.status !== "action") continue;
|
|
332
|
+
if (item.sourceKinds?.length) console.log(` Source role: ${item.sourceKinds.join(" or ")}`);
|
|
333
|
+
for (const source of item.sourceSystemChecks || []) {
|
|
334
|
+
const missing = Object.entries(source.checks)
|
|
335
|
+
.filter(([, passed]) => !passed)
|
|
336
|
+
.map(([name]) => evidenceSourceCheckName(name));
|
|
337
|
+
if (missing.length) console.log(` ${source.sourceSystemId}: ${missing.join(", ")}`);
|
|
338
|
+
}
|
|
339
|
+
if (item.commands?.length) console.log(` Next: ${item.commands[0]}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
302
342
|
return result;
|
|
303
343
|
}
|
|
304
344
|
if (command === "audit-readiness") {
|
|
@@ -337,7 +377,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
337
377
|
else {
|
|
338
378
|
console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
|
|
339
379
|
console.log(`Event: obligation-event/${result.event.id}`);
|
|
340
|
-
for (const action of result.actions)
|
|
380
|
+
for (const action of result.actions) {
|
|
381
|
+
const deadline = action.completionWindow?.dueAt || action.completionWindow?.dueOn;
|
|
382
|
+
console.log(`Task: action-item/${action.id}\t${action.title}\t${deadline}`);
|
|
383
|
+
}
|
|
341
384
|
}
|
|
342
385
|
return result;
|
|
343
386
|
}
|
|
@@ -418,7 +461,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
418
461
|
obligationId,
|
|
419
462
|
record: mutation.record,
|
|
420
463
|
content: mutation.content,
|
|
421
|
-
expectedRevision: flags
|
|
464
|
+
expectedRevision: requireExpectedRevision(flags, `obligation/${obligationId}`)
|
|
422
465
|
});
|
|
423
466
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
424
467
|
else console.log(`Created ${result.created.type}/${result.created.id} and linked it to obligation/${obligationId}`);
|
|
@@ -432,7 +475,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
432
475
|
completedOn: flags["completed-on"],
|
|
433
476
|
record: mutation.record,
|
|
434
477
|
content: mutation.content,
|
|
435
|
-
expectedRevision: flags
|
|
478
|
+
expectedRevision: requireExpectedRevision(flags, `action-item/${actionItemId}`)
|
|
436
479
|
});
|
|
437
480
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
438
481
|
else console.log(`Created ${result.created.type}/${result.created.id}, linked it to action-item/${actionItemId}, and marked the action done.`);
|
|
@@ -444,7 +487,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
444
487
|
const result = await completeObligationEvent(root, {
|
|
445
488
|
eventId,
|
|
446
489
|
completedOn: flags["completed-on"],
|
|
447
|
-
expectedRevision: flags
|
|
490
|
+
expectedRevision: requireExpectedRevision(flags, `obligation-event/${eventId}`)
|
|
448
491
|
});
|
|
449
492
|
if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
|
|
450
493
|
else console.log(`Marked obligation-event/${eventId} complete.`);
|
|
@@ -452,11 +495,12 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
452
495
|
}
|
|
453
496
|
if (command === "update") {
|
|
454
497
|
const [type, id, file] = positionals;
|
|
455
|
-
const mutation = await readMutation(file);
|
|
498
|
+
const mutation = await readMutation(file, { requireRevision: true });
|
|
456
499
|
const result = await updateResource(root, type, id, mutation.record, {
|
|
457
500
|
content: mutation.content,
|
|
458
501
|
expectedRevision: mutation.revision,
|
|
459
|
-
expectedContentRevisions: mutation.contentRevisions
|
|
502
|
+
expectedContentRevisions: mutation.contentRevisions,
|
|
503
|
+
requireExpectedContentRevisions: true
|
|
460
504
|
});
|
|
461
505
|
if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
|
|
462
506
|
else console.log(`Updated ${result.record.type}/${result.record.id}`);
|
|
@@ -507,7 +551,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
507
551
|
if (!evidenceId || !sourcePath) throw new Error("An evidence ID and source file are required.");
|
|
508
552
|
const result = await addEvidenceAttachment(root, evidenceId, sourcePath, {
|
|
509
553
|
name: flags.name,
|
|
510
|
-
expectedRevision: flags
|
|
554
|
+
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
511
555
|
});
|
|
512
556
|
const output = {
|
|
513
557
|
evidenceId,
|
|
@@ -523,7 +567,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
523
567
|
if (!evidenceId || !attachment) throw new Error("An evidence ID and attachment name are required.");
|
|
524
568
|
if (!flags.yes) throw new Error("Pass --yes to confirm attachment removal.");
|
|
525
569
|
const result = await removeEvidenceAttachment(root, evidenceId, attachment, {
|
|
526
|
-
expectedRevision: flags
|
|
570
|
+
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
527
571
|
});
|
|
528
572
|
const output = {
|
|
529
573
|
evidenceId,
|
|
@@ -537,7 +581,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
537
581
|
if (command === "delete") {
|
|
538
582
|
const [type, id] = positionals;
|
|
539
583
|
if (!flags.yes) throw new Error("Pass --yes to confirm deletion. Preserve historical records unless this is a mistake or uncommitted draft.");
|
|
540
|
-
await deleteResource(root, type, id, {
|
|
584
|
+
await deleteResource(root, type, id, {
|
|
585
|
+
expectedRevision: requireExpectedRevision(flags, `${type}/${id}`)
|
|
586
|
+
});
|
|
541
587
|
console.log(`Deleted ${type}/${id}`);
|
|
542
588
|
return;
|
|
543
589
|
}
|
|
@@ -565,37 +611,18 @@ function parseArgs(args) {
|
|
|
565
611
|
return { positionals, flags };
|
|
566
612
|
}
|
|
567
613
|
|
|
568
|
-
async function readMutation(path) {
|
|
614
|
+
async function readMutation(path, options = {}) {
|
|
569
615
|
if (!path) throw new Error("A JSON file path or - is required.");
|
|
570
616
|
const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (!parsed.record || Array.isArray(parsed.record) || typeof parsed.record !== "object") {
|
|
579
|
-
throw new Error("Mutation record must be a JSON object.");
|
|
580
|
-
}
|
|
581
|
-
if (parsed.content !== undefined && (Array.isArray(parsed.content) || typeof parsed.content !== "object" || parsed.content === null)) {
|
|
582
|
-
throw new Error("Mutation content must be an object keyed by Markdown slot.");
|
|
583
|
-
}
|
|
584
|
-
if (parsed.revision !== undefined && typeof parsed.revision !== "string") {
|
|
585
|
-
throw new Error("Mutation revision must be a string.");
|
|
586
|
-
}
|
|
587
|
-
if (
|
|
588
|
-
parsed.contentRevisions !== undefined
|
|
589
|
-
&& (Array.isArray(parsed.contentRevisions) || typeof parsed.contentRevisions !== "object" || parsed.contentRevisions === null)
|
|
590
|
-
) {
|
|
591
|
-
throw new Error("Mutation contentRevisions must be an object keyed by data-relative Markdown path.");
|
|
617
|
+
return normalizeResourceMutation(JSON.parse(source), options);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function requireExpectedRevision(flags, target) {
|
|
621
|
+
const revision = flags["expected-revision"];
|
|
622
|
+
if (typeof revision !== "string" || revision.length === 0) {
|
|
623
|
+
throw new Error(`--expected-revision is required when changing ${target}. Reload the resource and try again.`);
|
|
592
624
|
}
|
|
593
|
-
return
|
|
594
|
-
record: parsed.record,
|
|
595
|
-
content: parsed.content,
|
|
596
|
-
revision: parsed.revision,
|
|
597
|
-
contentRevisions: parsed.contentRevisions
|
|
598
|
-
};
|
|
625
|
+
return revision;
|
|
599
626
|
}
|
|
600
627
|
|
|
601
628
|
async function readSetupPayload(path) {
|
|
@@ -641,11 +668,11 @@ async function completeInteractiveSetup(root, payload) {
|
|
|
641
668
|
activePeople[0].id
|
|
642
669
|
);
|
|
643
670
|
result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
|
|
644
|
-
result.
|
|
671
|
+
result.classificationId ||= classifications.length
|
|
645
672
|
? await askChoice(
|
|
646
673
|
"Data classification",
|
|
647
674
|
classifications,
|
|
648
|
-
classifications.includes("
|
|
675
|
+
classifications.includes("confidential") ? "confidential" : classifications[0]
|
|
649
676
|
)
|
|
650
677
|
: await askRequired("Data classification");
|
|
651
678
|
if (result.internetExposed === undefined) {
|
|
@@ -683,11 +710,12 @@ function printHelp() {
|
|
|
683
710
|
console.log(`filegrc - Git-native GRC workspace
|
|
684
711
|
|
|
685
712
|
Usage:
|
|
686
|
-
filegrc serve [root] [--host 127.0.0.1] [--port 8787]
|
|
713
|
+
filegrc serve [root] [--host 127.0.0.1] [--port 8787] [--allow-non-authoritative-writes]
|
|
687
714
|
filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
|
|
688
715
|
filegrc build [root] [--output .filegrc/site]
|
|
689
716
|
filegrc validate [root] [--json]
|
|
690
717
|
filegrc model [--json|--write-docs|--check-docs]
|
|
718
|
+
filegrc migrate --to-model 2 [--preview] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
691
719
|
filegrc describe <resource-type>
|
|
692
720
|
filegrc types [--json]
|
|
693
721
|
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
@@ -697,22 +725,22 @@ Usage:
|
|
|
697
725
|
filegrc search <query> [--type resource-type] [--json]
|
|
698
726
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
699
727
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
700
|
-
filegrc evidence-
|
|
728
|
+
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
701
729
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
702
730
|
filegrc prepare-audit <audit-id> [--json]
|
|
703
731
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
704
732
|
filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
705
733
|
filegrc get [resource-type] <id> [--mutation]
|
|
706
734
|
filegrc references <id> [--json]
|
|
707
|
-
filegrc create <
|
|
708
|
-
filegrc complete <obligation-id> <completion-record.json|->
|
|
709
|
-
filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD
|
|
710
|
-
filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD
|
|
711
|
-
filegrc update <resource-type> <id> <
|
|
735
|
+
filegrc create <mutation.json|-> [--json]
|
|
736
|
+
filegrc complete <obligation-id> <completion-record.json|-> --expected-revision hash [--json]
|
|
737
|
+
filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD --expected-revision hash [--json]
|
|
738
|
+
filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD --expected-revision hash [--json]
|
|
739
|
+
filegrc update <resource-type> <id> <mutation.json|-> [--json]
|
|
712
740
|
filegrc content <resource-type> <id> [slot] [--write markdown-file|-] [--expected-revision hash] [--json]
|
|
713
|
-
filegrc attach <evidence-id> <source-file> [--name file-name] [--
|
|
714
|
-
filegrc detach <evidence-id> <attachment-name> --yes
|
|
715
|
-
filegrc delete <resource-type> <id> --yes
|
|
741
|
+
filegrc attach <evidence-id> <source-file> --expected-revision hash [--name file-name] [--json]
|
|
742
|
+
filegrc detach <evidence-id> <attachment-name> --yes --expected-revision hash [--json]
|
|
743
|
+
filegrc delete <resource-type> <id> --yes --expected-revision hash
|
|
716
744
|
|
|
717
745
|
All commands accept --root <workspace>. Writes never create Git commits.`);
|
|
718
746
|
}
|
|
@@ -720,12 +748,15 @@ All commands accept --root <workspace>. Writes never create Git commits.`);
|
|
|
720
748
|
function printCommandHelp(command) {
|
|
721
749
|
if (command === "serve") {
|
|
722
750
|
console.log(`Usage:
|
|
723
|
-
filegrc serve [root] [--host address] [--port number]
|
|
751
|
+
filegrc serve [root] [--host address] [--port number] [--allow-non-authoritative-writes]
|
|
724
752
|
|
|
725
753
|
Options:
|
|
726
754
|
--host <address> Bind address. Defaults to FILEGRC_HOST or 127.0.0.1.
|
|
727
755
|
--port <number> Port. Defaults to FILEGRC_PORT or 8787. Use 0 for an available port.
|
|
728
756
|
--root <path> Workspace path when no positional root is given.
|
|
757
|
+
--allow-non-authoritative-writes
|
|
758
|
+
Allow local browser writes from a task checkout. This explicit
|
|
759
|
+
development override never commits or pushes.
|
|
729
760
|
--help Show this help without starting the server.
|
|
730
761
|
|
|
731
762
|
Safety:
|
|
@@ -746,7 +777,7 @@ Options:
|
|
|
746
777
|
--boundary <description> boundary
|
|
747
778
|
--owner <person-id> ownerId
|
|
748
779
|
--criticality <level> low, medium, high, or critical
|
|
749
|
-
--classification <
|
|
780
|
+
--classification <id> classificationId
|
|
750
781
|
--internet-exposed <bool> true or false
|
|
751
782
|
--program-goal <goal> none, readiness, type-1, or type-2
|
|
752
783
|
--draft Save the service boundary as planned
|
|
@@ -757,14 +788,37 @@ Options:
|
|
|
757
788
|
--help Show this help`);
|
|
758
789
|
return;
|
|
759
790
|
}
|
|
791
|
+
if (command === "migrate") {
|
|
792
|
+
console.log(`Usage:
|
|
793
|
+
filegrc migrate --to-model 2 [options]
|
|
794
|
+
|
|
795
|
+
Upgrade a model v1 workspace to model v2. The migration moves reverse
|
|
796
|
+
relationships to their authoritative records, converts the former Policy Owner
|
|
797
|
+
seed role into a dated Appointment, makes repository behavior explicit, rewrites
|
|
798
|
+
the moved program-page ID, removes obsolete collection-test fields and per-record
|
|
799
|
+
schemaVersion keys, and changes dataModelVersion last. It writes no Git commit.
|
|
800
|
+
|
|
801
|
+
Options:
|
|
802
|
+
--to-model <version> Required target model; currently 2
|
|
803
|
+
--preview Show the complete atomic record plan without writing
|
|
804
|
+
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
805
|
+
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
806
|
+
--yes Apply the reviewed migration
|
|
807
|
+
--json Print the plan or result as JSON
|
|
808
|
+
--root <path> Workspace path
|
|
809
|
+
--help Show this help
|
|
810
|
+
|
|
811
|
+
Start with:
|
|
812
|
+
npx filegrc migrate --to-model 2 --preview --json`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
760
815
|
if (command === "program-readiness") {
|
|
761
816
|
console.log(`Usage:
|
|
762
817
|
filegrc program-readiness [options]
|
|
763
818
|
|
|
764
819
|
Report whether management has defined scope, activated policies, implemented
|
|
765
|
-
controls,
|
|
766
|
-
|
|
767
|
-
is required.
|
|
820
|
+
controls, and mapped every selected control to a configured authoritative evidence
|
|
821
|
+
source. No audit ID or CPA firm is required.
|
|
768
822
|
|
|
769
823
|
Options:
|
|
770
824
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
@@ -779,14 +833,14 @@ Options:
|
|
|
779
833
|
console.log(`Usage:
|
|
780
834
|
filegrc program-path [audit-id] [options]
|
|
781
835
|
|
|
782
|
-
Show the same
|
|
836
|
+
Show the same five-step SOC 2 lifecycle used by the renderer. Each step includes
|
|
783
837
|
its exact page instructions, Use and Policy Basis context, resource commands,
|
|
784
|
-
and current readiness state. Pass an audit ID to include Step
|
|
838
|
+
and current readiness state. Pass an audit ID to include Step 5 status.
|
|
785
839
|
|
|
786
840
|
Options:
|
|
787
|
-
--audit <id> Audit record to use for Step
|
|
841
|
+
--audit <id> Audit record to use for Step 5
|
|
788
842
|
--as-of <date> Evaluate readiness on YYYY-MM-DD
|
|
789
|
-
--summary Print compact status and the first action for all
|
|
843
|
+
--summary Print compact status and the first action for all five steps
|
|
790
844
|
--next Print only the current step and its first action
|
|
791
845
|
--current Print the full guide for the current step only
|
|
792
846
|
--json Print the selected path view as JSON
|
|
@@ -794,18 +848,18 @@ Options:
|
|
|
794
848
|
--help Show this help`);
|
|
795
849
|
return;
|
|
796
850
|
}
|
|
797
|
-
if (command === "evidence-
|
|
851
|
+
if (command === "evidence-map") {
|
|
798
852
|
console.log(`Usage:
|
|
799
|
-
filegrc evidence-
|
|
853
|
+
filegrc evidence-map [options]
|
|
800
854
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
855
|
+
Inspect the evidence-source checks included in Control implementation. Each item
|
|
856
|
+
reports the required source roles, linked Controls, authoritative source Systems,
|
|
857
|
+
per-record checks, and exact edit commands. This diagnostic is read-only.
|
|
804
858
|
|
|
805
859
|
Options:
|
|
806
|
-
--
|
|
807
|
-
--json
|
|
808
|
-
--root <path>
|
|
860
|
+
--as-of <date> Evaluate the map on YYYY-MM-DD
|
|
861
|
+
--json Print the map as JSON
|
|
862
|
+
--root <path> Workspace path
|
|
809
863
|
--help Show this help`);
|
|
810
864
|
return;
|
|
811
865
|
}
|
|
@@ -821,6 +875,7 @@ function agentOverview(model) {
|
|
|
821
875
|
build: "filegrc build [root]",
|
|
822
876
|
validate: "filegrc validate [root] --json",
|
|
823
877
|
model: "filegrc model --json",
|
|
878
|
+
migrate: "filegrc migrate --to-model 2 --preview --json",
|
|
824
879
|
describe: "filegrc describe <resource-type>",
|
|
825
880
|
types: "filegrc types --json",
|
|
826
881
|
guide: "filegrc guide [resource-type] --json",
|
|
@@ -830,18 +885,18 @@ function agentOverview(model) {
|
|
|
830
885
|
search: "filegrc search <query> --json",
|
|
831
886
|
obligations: "filegrc obligations --json",
|
|
832
887
|
programReadiness: "filegrc program-readiness --json",
|
|
833
|
-
|
|
888
|
+
evidenceMap: "filegrc evidence-map --json",
|
|
834
889
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
835
890
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
|
836
891
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
837
892
|
evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
|
|
838
893
|
get: "filegrc get <resource-id> [--mutation]",
|
|
839
894
|
references: "filegrc references <resource-id> --json",
|
|
840
|
-
create: "filegrc create <
|
|
895
|
+
create: "filegrc create <mutation.json>",
|
|
841
896
|
complete: "filegrc complete <obligation-id> <completion-mutation.json>",
|
|
842
897
|
completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
|
|
843
898
|
completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
|
|
844
|
-
update: "filegrc update <resource-type> <id> <
|
|
899
|
+
update: "filegrc update <resource-type> <id> <mutation.json>",
|
|
845
900
|
content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
|
|
846
901
|
attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
|
|
847
902
|
detach: "filegrc detach <evidence-id> <attachment-name> --yes",
|
|
@@ -917,6 +972,8 @@ function printAgentGuide(result) {
|
|
|
917
972
|
}
|
|
918
973
|
console.log("\nWorkflow:");
|
|
919
974
|
result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
|
|
975
|
+
console.log("\nCompletion checks:");
|
|
976
|
+
result.completionChecks.forEach((check) => console.log(`- ${check}`));
|
|
920
977
|
}
|
|
921
978
|
|
|
922
979
|
function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
@@ -1064,6 +1121,15 @@ function shellArgument(value) {
|
|
|
1064
1121
|
: `'${text.replaceAll("'", "'\\''")}'`;
|
|
1065
1122
|
}
|
|
1066
1123
|
|
|
1124
|
+
function evidenceSourceCheckName(name) {
|
|
1125
|
+
return ({
|
|
1126
|
+
active: "activate source",
|
|
1127
|
+
sourceRole: "add source role",
|
|
1128
|
+
accessOwners: "add access owner",
|
|
1129
|
+
retrievalInstructions: "add retrieval instructions"
|
|
1130
|
+
})[name] || name;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1067
1133
|
function printProgramPathOutput(result, flags) {
|
|
1068
1134
|
if (flags.summary) {
|
|
1069
1135
|
console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
|
|
@@ -1133,13 +1199,10 @@ function summarizeProgramReadiness(result) {
|
|
|
1133
1199
|
}
|
|
1134
1200
|
|
|
1135
1201
|
function eventWindowText(window) {
|
|
1136
|
-
if (Number.isInteger(window?.
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
return window.endOffsetDays === 0 ? "due on event date" : `due within ${window.endOffsetDays} days`;
|
|
1141
|
-
}
|
|
1142
|
-
return "due within 30 days";
|
|
1202
|
+
if (!Number.isInteger(window?.dueAfter)) return "deadline not configured";
|
|
1203
|
+
const unit = window.precision === "timestamp" ? "hour" : "day";
|
|
1204
|
+
if (window.dueAfter === 0) return window.precision === "timestamp" ? "due at event time" : "due on event date";
|
|
1205
|
+
return `due within ${window.dueAfter} ${unit}${window.dueAfter === 1 ? "" : "s"}`;
|
|
1143
1206
|
}
|
|
1144
1207
|
|
|
1145
1208
|
function formatGuideField(field) {
|
package/src/coverage.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function coverageBounds(coverage) {
|
|
2
|
+
if (coverage?.kind === "as-of" && typeof coverage.on === "string") {
|
|
3
|
+
return { start: coverage.on, end: coverage.on };
|
|
4
|
+
}
|
|
5
|
+
if (
|
|
6
|
+
coverage?.kind === "range"
|
|
7
|
+
&& typeof coverage.startsOn === "string"
|
|
8
|
+
&& typeof coverage.endsOn === "string"
|
|
9
|
+
) {
|
|
10
|
+
return { start: coverage.startsOn, end: coverage.endsOn };
|
|
11
|
+
}
|
|
12
|
+
return { start: null, end: null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function coverageStart(coverage) {
|
|
16
|
+
return coverageBounds(coverage).start;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function coverageEnd(coverage) {
|
|
20
|
+
return coverageBounds(coverage).end;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function coverageMatches(coverage, start, end = start) {
|
|
24
|
+
const bounds = coverageBounds(coverage);
|
|
25
|
+
return bounds.start === start && bounds.end === end;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function coverageOverlaps(coverage, start, end = start) {
|
|
29
|
+
const bounds = coverageBounds(coverage);
|
|
30
|
+
return Boolean(bounds.start && bounds.end && bounds.start <= end && bounds.end >= start);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function coverageContains(coverage, date) {
|
|
34
|
+
return coverageOverlaps(coverage, date, date);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function coverageLabel(coverage) {
|
|
38
|
+
const bounds = coverageBounds(coverage);
|
|
39
|
+
if (!bounds.start || !bounds.end) return "";
|
|
40
|
+
return bounds.start === bounds.end ? bounds.start : `${bounds.start} through ${bounds.end}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function legacyCoverage(record, options = {}) {
|
|
44
|
+
const asOf = options.asOfFields?.map((field) => record[field]).find(Boolean);
|
|
45
|
+
if (asOf) return { kind: "as-of", on: asOf };
|
|
46
|
+
const start = options.startFields?.map((field) => record[field]).find(Boolean);
|
|
47
|
+
const end = options.endFields?.map((field) => record[field]).find(Boolean);
|
|
48
|
+
if (start && end) return { kind: "range", startsOn: start, endsOn: end };
|
|
49
|
+
return null;
|
|
50
|
+
}
|