filegrc 0.3.4 → 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 +16 -6
- package/model/index.js +37 -3
- package/model/v1.json +81 -47
- 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 +168 -110
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +115 -75
- package/src/files.js +230 -28
- package/src/git.js +239 -41
- package/src/index.js +5 -5
- package/src/model-docs.js +88 -7
- package/src/model-migration.js +1463 -0
- package/src/mutation.js +42 -0
- 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 +110 -39
- package/src/setup.js +27 -28
- package/src/state.js +86 -25
- package/src/timing.js +41 -0
- package/src/validate.js +609 -43
- package/src/web.js +506 -129
- 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";
|
|
@@ -89,7 +91,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
89
91
|
...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
|
|
90
92
|
...(flags.owner !== undefined ? { ownerId: flags.owner } : {}),
|
|
91
93
|
...(flags.criticality !== undefined ? { criticality: flags.criticality } : {}),
|
|
92
|
-
...(flags.classification !== undefined ? {
|
|
94
|
+
...(flags.classification !== undefined ? { classificationId: flags.classification } : {}),
|
|
93
95
|
...(flags["internet-exposed"] !== undefined ? { internetExposed: flags["internet-exposed"] } : {}),
|
|
94
96
|
...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
|
|
95
97
|
...(flags.draft ? { draft: true } : {})
|
|
@@ -101,7 +103,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
101
103
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
102
104
|
else if (flags.preview) {
|
|
103
105
|
console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
|
|
104
|
-
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.");
|
|
105
107
|
}
|
|
106
108
|
else {
|
|
107
109
|
console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
|
|
@@ -144,6 +146,35 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
144
146
|
else console.log(source);
|
|
145
147
|
return;
|
|
146
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
|
+
}
|
|
147
178
|
if (command === "describe") {
|
|
148
179
|
const loaded = await loadWorkspace(root);
|
|
149
180
|
const type = positionals[0];
|
|
@@ -195,11 +226,18 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
195
226
|
const loaded = await loadWorkspace(root);
|
|
196
227
|
const type = positionals[0];
|
|
197
228
|
if (type && !loaded.model.resources[type]) throw new Error(`Unknown resource type "${type}".`);
|
|
229
|
+
const asOf = currentCalendarDate(loaded.workspace.timezone);
|
|
198
230
|
const records = loaded.resources
|
|
199
231
|
.filter((record) => !type || record.type === type)
|
|
200
|
-
.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
|
+
});
|
|
201
239
|
if (flags.json) console.log(JSON.stringify(records, null, 2));
|
|
202
|
-
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}`);
|
|
203
241
|
return records;
|
|
204
242
|
}
|
|
205
243
|
if (command === "search") {
|
|
@@ -217,7 +255,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
217
255
|
from: flags.from,
|
|
218
256
|
through: flags.through,
|
|
219
257
|
now: flags.now,
|
|
220
|
-
includeComplete: Boolean(flags.complete)
|
|
258
|
+
includeComplete: Boolean(flags.complete),
|
|
259
|
+
model: loaded.model
|
|
221
260
|
});
|
|
222
261
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
223
262
|
else {
|
|
@@ -236,7 +275,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
236
275
|
if (result.triggers.length) {
|
|
237
276
|
console.log("\nPolicy Events:");
|
|
238
277
|
for (const trigger of result.triggers) {
|
|
239
|
-
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"}`);
|
|
240
279
|
for (const step of trigger.steps) {
|
|
241
280
|
const owners = step.ownerIds.length ? step.ownerIds.join(",") : "unassigned";
|
|
242
281
|
const proof = step.completionResourceTypes.length ? step.completionResourceTypes.join("|") : "not specified";
|
|
@@ -262,7 +301,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
262
301
|
}
|
|
263
302
|
else {
|
|
264
303
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
|
|
265
|
-
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
|
+
);
|
|
266
312
|
for (const stage of result.stages) {
|
|
267
313
|
console.log(`\n${stage.title}`);
|
|
268
314
|
for (const item of stage.items) console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
|
|
@@ -274,33 +320,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
274
320
|
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
275
321
|
return output;
|
|
276
322
|
}
|
|
277
|
-
if (command === "evidence-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const plan = planEvidenceTestDrafts(loaded);
|
|
281
|
-
const result = {
|
|
282
|
-
schemaVersion: 1,
|
|
283
|
-
preview: true,
|
|
284
|
-
total: plan.length,
|
|
285
|
-
create: plan.filter(({ existing }) => !existing).map((item) => ({
|
|
286
|
-
familyId: item.familyId,
|
|
287
|
-
title: item.title,
|
|
288
|
-
testEvidenceKind: item.testEvidenceKind,
|
|
289
|
-
controlIds: item.controlIds
|
|
290
|
-
})),
|
|
291
|
-
existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
|
|
292
|
-
id: existing.id,
|
|
293
|
-
title: existing.title,
|
|
294
|
-
status: existing.status
|
|
295
|
-
}))
|
|
296
|
-
};
|
|
297
|
-
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
298
|
-
else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
|
|
299
|
-
return result;
|
|
300
|
-
}
|
|
301
|
-
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"] });
|
|
302
326
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
303
|
-
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
|
+
}
|
|
304
342
|
return result;
|
|
305
343
|
}
|
|
306
344
|
if (command === "audit-readiness") {
|
|
@@ -339,7 +377,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
339
377
|
else {
|
|
340
378
|
console.log(`Work added to the Work Queue: ${result.actions.length} ${result.actions.length === 1 ? "task" : "tasks"} created for ${result.event.title}.`);
|
|
341
379
|
console.log(`Event: obligation-event/${result.event.id}`);
|
|
342
|
-
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
|
+
}
|
|
343
384
|
}
|
|
344
385
|
return result;
|
|
345
386
|
}
|
|
@@ -420,7 +461,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
420
461
|
obligationId,
|
|
421
462
|
record: mutation.record,
|
|
422
463
|
content: mutation.content,
|
|
423
|
-
expectedRevision: flags
|
|
464
|
+
expectedRevision: requireExpectedRevision(flags, `obligation/${obligationId}`)
|
|
424
465
|
});
|
|
425
466
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
426
467
|
else console.log(`Created ${result.created.type}/${result.created.id} and linked it to obligation/${obligationId}`);
|
|
@@ -434,7 +475,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
434
475
|
completedOn: flags["completed-on"],
|
|
435
476
|
record: mutation.record,
|
|
436
477
|
content: mutation.content,
|
|
437
|
-
expectedRevision: flags
|
|
478
|
+
expectedRevision: requireExpectedRevision(flags, `action-item/${actionItemId}`)
|
|
438
479
|
});
|
|
439
480
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
440
481
|
else console.log(`Created ${result.created.type}/${result.created.id}, linked it to action-item/${actionItemId}, and marked the action done.`);
|
|
@@ -446,7 +487,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
446
487
|
const result = await completeObligationEvent(root, {
|
|
447
488
|
eventId,
|
|
448
489
|
completedOn: flags["completed-on"],
|
|
449
|
-
expectedRevision: flags
|
|
490
|
+
expectedRevision: requireExpectedRevision(flags, `obligation-event/${eventId}`)
|
|
450
491
|
});
|
|
451
492
|
if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
|
|
452
493
|
else console.log(`Marked obligation-event/${eventId} complete.`);
|
|
@@ -454,11 +495,12 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
454
495
|
}
|
|
455
496
|
if (command === "update") {
|
|
456
497
|
const [type, id, file] = positionals;
|
|
457
|
-
const mutation = await readMutation(file);
|
|
498
|
+
const mutation = await readMutation(file, { requireRevision: true });
|
|
458
499
|
const result = await updateResource(root, type, id, mutation.record, {
|
|
459
500
|
content: mutation.content,
|
|
460
501
|
expectedRevision: mutation.revision,
|
|
461
|
-
expectedContentRevisions: mutation.contentRevisions
|
|
502
|
+
expectedContentRevisions: mutation.contentRevisions,
|
|
503
|
+
requireExpectedContentRevisions: true
|
|
462
504
|
});
|
|
463
505
|
if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
|
|
464
506
|
else console.log(`Updated ${result.record.type}/${result.record.id}`);
|
|
@@ -509,7 +551,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
509
551
|
if (!evidenceId || !sourcePath) throw new Error("An evidence ID and source file are required.");
|
|
510
552
|
const result = await addEvidenceAttachment(root, evidenceId, sourcePath, {
|
|
511
553
|
name: flags.name,
|
|
512
|
-
expectedRevision: flags
|
|
554
|
+
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
513
555
|
});
|
|
514
556
|
const output = {
|
|
515
557
|
evidenceId,
|
|
@@ -525,7 +567,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
525
567
|
if (!evidenceId || !attachment) throw new Error("An evidence ID and attachment name are required.");
|
|
526
568
|
if (!flags.yes) throw new Error("Pass --yes to confirm attachment removal.");
|
|
527
569
|
const result = await removeEvidenceAttachment(root, evidenceId, attachment, {
|
|
528
|
-
expectedRevision: flags
|
|
570
|
+
expectedRevision: requireExpectedRevision(flags, `evidence/${evidenceId}`)
|
|
529
571
|
});
|
|
530
572
|
const output = {
|
|
531
573
|
evidenceId,
|
|
@@ -539,7 +581,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
539
581
|
if (command === "delete") {
|
|
540
582
|
const [type, id] = positionals;
|
|
541
583
|
if (!flags.yes) throw new Error("Pass --yes to confirm deletion. Preserve historical records unless this is a mistake or uncommitted draft.");
|
|
542
|
-
await deleteResource(root, type, id, {
|
|
584
|
+
await deleteResource(root, type, id, {
|
|
585
|
+
expectedRevision: requireExpectedRevision(flags, `${type}/${id}`)
|
|
586
|
+
});
|
|
543
587
|
console.log(`Deleted ${type}/${id}`);
|
|
544
588
|
return;
|
|
545
589
|
}
|
|
@@ -567,37 +611,18 @@ function parseArgs(args) {
|
|
|
567
611
|
return { positionals, flags };
|
|
568
612
|
}
|
|
569
613
|
|
|
570
|
-
async function readMutation(path) {
|
|
614
|
+
async function readMutation(path, options = {}) {
|
|
571
615
|
if (!path) throw new Error("A JSON file path or - is required.");
|
|
572
616
|
const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (!parsed.record || Array.isArray(parsed.record) || typeof parsed.record !== "object") {
|
|
581
|
-
throw new Error("Mutation record must be a JSON object.");
|
|
582
|
-
}
|
|
583
|
-
if (parsed.content !== undefined && (Array.isArray(parsed.content) || typeof parsed.content !== "object" || parsed.content === null)) {
|
|
584
|
-
throw new Error("Mutation content must be an object keyed by Markdown slot.");
|
|
585
|
-
}
|
|
586
|
-
if (parsed.revision !== undefined && typeof parsed.revision !== "string") {
|
|
587
|
-
throw new Error("Mutation revision must be a string.");
|
|
588
|
-
}
|
|
589
|
-
if (
|
|
590
|
-
parsed.contentRevisions !== undefined
|
|
591
|
-
&& (Array.isArray(parsed.contentRevisions) || typeof parsed.contentRevisions !== "object" || parsed.contentRevisions === null)
|
|
592
|
-
) {
|
|
593
|
-
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.`);
|
|
594
624
|
}
|
|
595
|
-
return
|
|
596
|
-
record: parsed.record,
|
|
597
|
-
content: parsed.content,
|
|
598
|
-
revision: parsed.revision,
|
|
599
|
-
contentRevisions: parsed.contentRevisions
|
|
600
|
-
};
|
|
625
|
+
return revision;
|
|
601
626
|
}
|
|
602
627
|
|
|
603
628
|
async function readSetupPayload(path) {
|
|
@@ -643,11 +668,11 @@ async function completeInteractiveSetup(root, payload) {
|
|
|
643
668
|
activePeople[0].id
|
|
644
669
|
);
|
|
645
670
|
result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
|
|
646
|
-
result.
|
|
671
|
+
result.classificationId ||= classifications.length
|
|
647
672
|
? await askChoice(
|
|
648
673
|
"Data classification",
|
|
649
674
|
classifications,
|
|
650
|
-
classifications.includes("
|
|
675
|
+
classifications.includes("confidential") ? "confidential" : classifications[0]
|
|
651
676
|
)
|
|
652
677
|
: await askRequired("Data classification");
|
|
653
678
|
if (result.internetExposed === undefined) {
|
|
@@ -690,6 +715,7 @@ Usage:
|
|
|
690
715
|
filegrc build [root] [--output .filegrc/site]
|
|
691
716
|
filegrc validate [root] [--json]
|
|
692
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]
|
|
693
719
|
filegrc describe <resource-type>
|
|
694
720
|
filegrc types [--json]
|
|
695
721
|
filegrc guide [resource-type] [--id resource-id] [--json]
|
|
@@ -699,22 +725,22 @@ Usage:
|
|
|
699
725
|
filegrc search <query> [--type resource-type] [--json]
|
|
700
726
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
701
727
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
702
|
-
filegrc evidence-
|
|
728
|
+
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
703
729
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
704
730
|
filegrc prepare-audit <audit-id> [--json]
|
|
705
731
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
706
732
|
filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
707
733
|
filegrc get [resource-type] <id> [--mutation]
|
|
708
734
|
filegrc references <id> [--json]
|
|
709
|
-
filegrc create <
|
|
710
|
-
filegrc complete <obligation-id> <completion-record.json|->
|
|
711
|
-
filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD
|
|
712
|
-
filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD
|
|
713
|
-
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]
|
|
714
740
|
filegrc content <resource-type> <id> [slot] [--write markdown-file|-] [--expected-revision hash] [--json]
|
|
715
|
-
filegrc attach <evidence-id> <source-file> [--name file-name] [--
|
|
716
|
-
filegrc detach <evidence-id> <attachment-name> --yes
|
|
717
|
-
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
|
|
718
744
|
|
|
719
745
|
All commands accept --root <workspace>. Writes never create Git commits.`);
|
|
720
746
|
}
|
|
@@ -751,7 +777,7 @@ Options:
|
|
|
751
777
|
--boundary <description> boundary
|
|
752
778
|
--owner <person-id> ownerId
|
|
753
779
|
--criticality <level> low, medium, high, or critical
|
|
754
|
-
--classification <
|
|
780
|
+
--classification <id> classificationId
|
|
755
781
|
--internet-exposed <bool> true or false
|
|
756
782
|
--program-goal <goal> none, readiness, type-1, or type-2
|
|
757
783
|
--draft Save the service boundary as planned
|
|
@@ -762,14 +788,37 @@ Options:
|
|
|
762
788
|
--help Show this help`);
|
|
763
789
|
return;
|
|
764
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
|
+
}
|
|
765
815
|
if (command === "program-readiness") {
|
|
766
816
|
console.log(`Usage:
|
|
767
817
|
filegrc program-readiness [options]
|
|
768
818
|
|
|
769
819
|
Report whether management has defined scope, activated policies, implemented
|
|
770
|
-
controls,
|
|
771
|
-
|
|
772
|
-
is required.
|
|
820
|
+
controls, and mapped every selected control to a configured authoritative evidence
|
|
821
|
+
source. No audit ID or CPA firm is required.
|
|
773
822
|
|
|
774
823
|
Options:
|
|
775
824
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
@@ -784,14 +833,14 @@ Options:
|
|
|
784
833
|
console.log(`Usage:
|
|
785
834
|
filegrc program-path [audit-id] [options]
|
|
786
835
|
|
|
787
|
-
Show the same
|
|
836
|
+
Show the same five-step SOC 2 lifecycle used by the renderer. Each step includes
|
|
788
837
|
its exact page instructions, Use and Policy Basis context, resource commands,
|
|
789
|
-
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.
|
|
790
839
|
|
|
791
840
|
Options:
|
|
792
|
-
--audit <id> Audit record to use for Step
|
|
841
|
+
--audit <id> Audit record to use for Step 5
|
|
793
842
|
--as-of <date> Evaluate readiness on YYYY-MM-DD
|
|
794
|
-
--summary Print compact status and the first action for all
|
|
843
|
+
--summary Print compact status and the first action for all five steps
|
|
795
844
|
--next Print only the current step and its first action
|
|
796
845
|
--current Print the full guide for the current step only
|
|
797
846
|
--json Print the selected path view as JSON
|
|
@@ -799,18 +848,18 @@ Options:
|
|
|
799
848
|
--help Show this help`);
|
|
800
849
|
return;
|
|
801
850
|
}
|
|
802
|
-
if (command === "evidence-
|
|
851
|
+
if (command === "evidence-map") {
|
|
803
852
|
console.log(`Usage:
|
|
804
|
-
filegrc evidence-
|
|
853
|
+
filegrc evidence-map [options]
|
|
805
854
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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.
|
|
809
858
|
|
|
810
859
|
Options:
|
|
811
|
-
--
|
|
812
|
-
--json
|
|
813
|
-
--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
|
|
814
863
|
--help Show this help`);
|
|
815
864
|
return;
|
|
816
865
|
}
|
|
@@ -826,6 +875,7 @@ function agentOverview(model) {
|
|
|
826
875
|
build: "filegrc build [root]",
|
|
827
876
|
validate: "filegrc validate [root] --json",
|
|
828
877
|
model: "filegrc model --json",
|
|
878
|
+
migrate: "filegrc migrate --to-model 2 --preview --json",
|
|
829
879
|
describe: "filegrc describe <resource-type>",
|
|
830
880
|
types: "filegrc types --json",
|
|
831
881
|
guide: "filegrc guide [resource-type] --json",
|
|
@@ -835,18 +885,18 @@ function agentOverview(model) {
|
|
|
835
885
|
search: "filegrc search <query> --json",
|
|
836
886
|
obligations: "filegrc obligations --json",
|
|
837
887
|
programReadiness: "filegrc program-readiness --json",
|
|
838
|
-
|
|
888
|
+
evidenceMap: "filegrc evidence-map --json",
|
|
839
889
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
840
890
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
|
841
891
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
842
892
|
evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
|
|
843
893
|
get: "filegrc get <resource-id> [--mutation]",
|
|
844
894
|
references: "filegrc references <resource-id> --json",
|
|
845
|
-
create: "filegrc create <
|
|
895
|
+
create: "filegrc create <mutation.json>",
|
|
846
896
|
complete: "filegrc complete <obligation-id> <completion-mutation.json>",
|
|
847
897
|
completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
|
|
848
898
|
completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
|
|
849
|
-
update: "filegrc update <resource-type> <id> <
|
|
899
|
+
update: "filegrc update <resource-type> <id> <mutation.json>",
|
|
850
900
|
content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
|
|
851
901
|
attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
|
|
852
902
|
detach: "filegrc detach <evidence-id> <attachment-name> --yes",
|
|
@@ -922,6 +972,8 @@ function printAgentGuide(result) {
|
|
|
922
972
|
}
|
|
923
973
|
console.log("\nWorkflow:");
|
|
924
974
|
result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
|
|
975
|
+
console.log("\nCompletion checks:");
|
|
976
|
+
result.completionChecks.forEach((check) => console.log(`- ${check}`));
|
|
925
977
|
}
|
|
926
978
|
|
|
927
979
|
function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
@@ -1069,6 +1121,15 @@ function shellArgument(value) {
|
|
|
1069
1121
|
: `'${text.replaceAll("'", "'\\''")}'`;
|
|
1070
1122
|
}
|
|
1071
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
|
+
|
|
1072
1133
|
function printProgramPathOutput(result, flags) {
|
|
1073
1134
|
if (flags.summary) {
|
|
1074
1135
|
console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
|
|
@@ -1138,13 +1199,10 @@ function summarizeProgramReadiness(result) {
|
|
|
1138
1199
|
}
|
|
1139
1200
|
|
|
1140
1201
|
function eventWindowText(window) {
|
|
1141
|
-
if (Number.isInteger(window?.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
return window.endOffsetDays === 0 ? "due on event date" : `due within ${window.endOffsetDays} days`;
|
|
1146
|
-
}
|
|
1147
|
-
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"}`;
|
|
1148
1206
|
}
|
|
1149
1207
|
|
|
1150
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
|
+
}
|