filegrc 0.2.0 → 0.3.1
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/LICENSE +1 -1
- package/README.md +10 -9
- package/model/v1.json +21 -21
- package/package.json +2 -2
- package/src/audit-preparation.js +13 -36
- package/src/cli.js +154 -17
- package/src/evidence-packet.js +18 -18
- package/src/git.js +1 -1
- package/src/index.js +2 -2
- package/src/model-docs.js +1 -1
- package/src/parties.js +46 -0
- package/src/paths.js +1 -1
- package/src/program-lifecycle.js +3 -0
- package/src/program-path.js +9 -9
- package/src/program-readiness.js +80 -32
- package/src/server.js +2 -2
- package/src/setup.js +104 -66
- package/src/state.js +1 -1
- package/src/validate.js +5 -34
- package/src/web.js +98 -40
package/src/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { loadModel } from "../model/index.js";
|
|
4
5
|
import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
|
|
5
6
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
6
7
|
import { buildWorkspace } from "./build.js";
|
|
7
8
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
8
|
-
import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
|
|
9
|
+
import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
|
|
9
10
|
import {
|
|
10
11
|
addEvidenceAttachment,
|
|
11
12
|
createResource,
|
|
@@ -27,7 +28,7 @@ import { assessProgramReadiness } from "./program-readiness.js";
|
|
|
27
28
|
import { markdownEntries } from "./resource-markdown.js";
|
|
28
29
|
import { searchResources } from "./search.js";
|
|
29
30
|
import { serveWorkspace } from "./server.js";
|
|
30
|
-
import { setupWorkspace } from "./setup.js";
|
|
31
|
+
import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
31
32
|
import { createAppState } from "./state.js";
|
|
32
33
|
import { currentCalendarDate } from "./time.js";
|
|
33
34
|
import { validateWorkspace } from "./validate.js";
|
|
@@ -42,6 +43,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
42
43
|
"mutation",
|
|
43
44
|
"preview",
|
|
44
45
|
"require-ready",
|
|
46
|
+
"summary",
|
|
45
47
|
"write-docs",
|
|
46
48
|
"yes"
|
|
47
49
|
]);
|
|
@@ -60,7 +62,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
60
62
|
host: flags.host ?? process.env.FILEGRC_HOST,
|
|
61
63
|
port: flags.port ?? process.env.FILEGRC_PORT
|
|
62
64
|
});
|
|
63
|
-
console.log(`
|
|
65
|
+
console.log(`filegrc workspace: ${result.url}`);
|
|
64
66
|
console.log(`Data: ${result.root}/data`);
|
|
65
67
|
return await new Promise((resolvePromise) => {
|
|
66
68
|
const stop = () => result.server.close(resolvePromise);
|
|
@@ -70,7 +72,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
70
72
|
}
|
|
71
73
|
if (command === "setup") {
|
|
72
74
|
const payload = positionals[0] ? await readSetupPayload(positionals[0]) : {};
|
|
73
|
-
const
|
|
75
|
+
const setupInput = await completeInteractiveSetup(root, {
|
|
74
76
|
...payload,
|
|
75
77
|
...(flags["service-name"] !== undefined ? { serviceName: flags["service-name"] } : {}),
|
|
76
78
|
...(flags.boundary !== undefined ? { boundary: flags.boundary } : {}),
|
|
@@ -81,14 +83,22 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
81
83
|
...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
|
|
82
84
|
...(flags.draft ? { draft: true } : {})
|
|
83
85
|
});
|
|
84
|
-
|
|
86
|
+
const result = flags.preview
|
|
87
|
+
? await planWorkspaceSetup(root, setupInput)
|
|
88
|
+
: await setupWorkspace(root, setupInput);
|
|
89
|
+
const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
|
|
90
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
91
|
+
else if (flags.preview) {
|
|
92
|
+
console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
|
|
93
|
+
console.log("No controls will be linked and no evidence drafts will be created.");
|
|
94
|
+
}
|
|
85
95
|
else {
|
|
86
96
|
console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
|
|
87
97
|
console.log(`System: ${result.system.id} (${result.system.status})`);
|
|
88
98
|
console.log(`Target: ${result.workspace.assuranceGoal}`);
|
|
89
99
|
console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run filegrc program-path for the full path.");
|
|
90
100
|
}
|
|
91
|
-
return
|
|
101
|
+
return output;
|
|
92
102
|
}
|
|
93
103
|
if (command === "build") {
|
|
94
104
|
const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
|
|
@@ -226,7 +236,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
226
236
|
if (command === "program-readiness") {
|
|
227
237
|
const loaded = await loadWorkspace(root);
|
|
228
238
|
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"] });
|
|
229
|
-
|
|
239
|
+
const output = flags.summary ? summarizeProgramReadiness(result) : result;
|
|
240
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
241
|
+
else if (flags.summary) {
|
|
242
|
+
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
|
|
243
|
+
for (const stage of output.stages) {
|
|
244
|
+
console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} actions`);
|
|
245
|
+
}
|
|
246
|
+
if (output.firstAction) console.log(`Next: ${output.firstAction.title}\t${output.firstAction.message}`);
|
|
247
|
+
}
|
|
230
248
|
else {
|
|
231
249
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} program items complete`);
|
|
232
250
|
console.log(`${result.target.label}${result.target.candidatePeriodStart ? `, candidate period starts ${result.target.candidatePeriodStart}` : ""}`);
|
|
@@ -239,9 +257,32 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
239
257
|
}
|
|
240
258
|
}
|
|
241
259
|
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
242
|
-
return
|
|
260
|
+
return output;
|
|
243
261
|
}
|
|
244
262
|
if (command === "evidence-test-drafts") {
|
|
263
|
+
if (flags.preview) {
|
|
264
|
+
const loaded = await loadWorkspace(root);
|
|
265
|
+
const plan = planEvidenceTestDrafts(loaded);
|
|
266
|
+
const result = {
|
|
267
|
+
schemaVersion: 1,
|
|
268
|
+
preview: true,
|
|
269
|
+
total: plan.length,
|
|
270
|
+
create: plan.filter(({ existing }) => !existing).map((item) => ({
|
|
271
|
+
familyId: item.familyId,
|
|
272
|
+
title: item.title,
|
|
273
|
+
testEvidenceKind: item.testEvidenceKind,
|
|
274
|
+
controlIds: item.controlIds
|
|
275
|
+
})),
|
|
276
|
+
existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
|
|
277
|
+
id: existing.id,
|
|
278
|
+
title: existing.title,
|
|
279
|
+
status: existing.status
|
|
280
|
+
}))
|
|
281
|
+
};
|
|
282
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
283
|
+
else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
245
286
|
const result = await ensureEvidenceTestDrafts(root);
|
|
246
287
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
247
288
|
else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
|
|
@@ -554,6 +595,56 @@ async function readSetupPayload(path) {
|
|
|
554
595
|
return parsed;
|
|
555
596
|
}
|
|
556
597
|
|
|
598
|
+
async function completeInteractiveSetup(root, payload) {
|
|
599
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return payload;
|
|
600
|
+
const loaded = await loadWorkspace(root);
|
|
601
|
+
const activePeople = loaded.resources.filter(({ type, status }) => type === "person" && status === "active");
|
|
602
|
+
if (!activePeople.length) throw new Error("Setup requires at least one active person who can own the service.");
|
|
603
|
+
const classifications = Object.keys(loaded.workspace.classificationDefinitions || {});
|
|
604
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
605
|
+
const result = { ...payload };
|
|
606
|
+
const askRequired = async (label, defaultValue = "") => {
|
|
607
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
608
|
+
let value = "";
|
|
609
|
+
while (!value) value = (await prompt.question(`${label}${suffix}: `)).trim() || defaultValue;
|
|
610
|
+
return value;
|
|
611
|
+
};
|
|
612
|
+
const askChoice = async (label, choices, defaultValue = "") => {
|
|
613
|
+
let value = "";
|
|
614
|
+
while (!choices.includes(value)) {
|
|
615
|
+
value = await askRequired(`${label} (${choices.join("/")})`, defaultValue);
|
|
616
|
+
}
|
|
617
|
+
return value;
|
|
618
|
+
};
|
|
619
|
+
try {
|
|
620
|
+
result.serviceName ||= await askRequired(
|
|
621
|
+
"Service name",
|
|
622
|
+
loaded.workspace.organizationName ? `${loaded.workspace.organizationName} service` : ""
|
|
623
|
+
);
|
|
624
|
+
result.boundary ||= await askRequired("Service boundary");
|
|
625
|
+
result.ownerId ||= await askChoice(
|
|
626
|
+
"Service owner ID",
|
|
627
|
+
activePeople.map(({ id }) => id),
|
|
628
|
+
activePeople[0].id
|
|
629
|
+
);
|
|
630
|
+
result.criticality ||= await askChoice("Criticality", ["low", "medium", "high", "critical"], "high");
|
|
631
|
+
result.dataClassification ||= classifications.length
|
|
632
|
+
? await askChoice(
|
|
633
|
+
"Data classification",
|
|
634
|
+
classifications,
|
|
635
|
+
classifications.includes("Confidential") ? "Confidential" : classifications[0]
|
|
636
|
+
)
|
|
637
|
+
: await askRequired("Data classification");
|
|
638
|
+
if (result.internetExposed === undefined) {
|
|
639
|
+
result.internetExposed = (await askChoice("Internet exposed", ["yes", "no"])) === "yes";
|
|
640
|
+
}
|
|
641
|
+
result.programGoal ||= await askChoice("Program goal", ["none", "readiness", "type-1", "type-2"]);
|
|
642
|
+
} finally {
|
|
643
|
+
prompt.close();
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
|
|
557
648
|
async function readTextInput(path) {
|
|
558
649
|
if (path === true || !path) throw new Error("Pass --write <markdown-file|->.");
|
|
559
650
|
return path === "-" ? readStdin() : readFile(resolve(String(path)), "utf8");
|
|
@@ -576,11 +667,11 @@ async function printVersion() {
|
|
|
576
667
|
}
|
|
577
668
|
|
|
578
669
|
function printHelp() {
|
|
579
|
-
console.log(`
|
|
670
|
+
console.log(`filegrc - Git-native GRC workspace
|
|
580
671
|
|
|
581
672
|
Usage:
|
|
582
673
|
filegrc serve [root] [--host 127.0.0.1] [--port 8787]
|
|
583
|
-
filegrc setup [setup.json|-] [setup options] [--draft] [--json]
|
|
674
|
+
filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
|
|
584
675
|
filegrc build [root] [--output .filegrc/site]
|
|
585
676
|
filegrc validate [root] [--json]
|
|
586
677
|
filegrc model [--json|--write-docs|--check-docs]
|
|
@@ -592,8 +683,8 @@ Usage:
|
|
|
592
683
|
filegrc list [resource-type] [--json]
|
|
593
684
|
filegrc search <query> [--type resource-type] [--json]
|
|
594
685
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
595
|
-
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--json]
|
|
596
|
-
filegrc evidence-test-drafts [--json]
|
|
686
|
+
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
687
|
+
filegrc evidence-test-drafts [--preview] [--json]
|
|
597
688
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
598
689
|
filegrc prepare-audit <audit-id> [--json]
|
|
599
690
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
@@ -634,7 +725,8 @@ Safety:
|
|
|
634
725
|
filegrc setup [setup.json|-] [options]
|
|
635
726
|
|
|
636
727
|
Create or update the initial service boundary through the same validated operation
|
|
637
|
-
used by browser onboarding.
|
|
728
|
+
used by browser onboarding. Run without input in an interactive terminal for guided
|
|
729
|
+
setup. JSON keys use the camelCase forms shown below.
|
|
638
730
|
|
|
639
731
|
Options:
|
|
640
732
|
--service-name <name> serviceName
|
|
@@ -645,6 +737,8 @@ Options:
|
|
|
645
737
|
--internet-exposed <bool> true or false
|
|
646
738
|
--program-goal <goal> none, readiness, type-1, or type-2
|
|
647
739
|
--draft Save the service boundary as planned
|
|
740
|
+
--preview Validate and report planned writes without saving
|
|
741
|
+
--summary Omit full workspace relationship arrays
|
|
648
742
|
--json Print the result as JSON
|
|
649
743
|
--root <path> Workspace path
|
|
650
744
|
--help Show this help`);
|
|
@@ -662,6 +756,7 @@ is required.
|
|
|
662
756
|
Options:
|
|
663
757
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
664
758
|
--require-ready Exit with code 2 unless the Evidence Ready gate passes
|
|
759
|
+
--summary Omit item details and print stage counts and next actions
|
|
665
760
|
--json Print the result as JSON
|
|
666
761
|
--root <path> Workspace path
|
|
667
762
|
--help Show this help`);
|
|
@@ -687,10 +782,12 @@ Options:
|
|
|
687
782
|
console.log(`Usage:
|
|
688
783
|
filegrc evidence-test-drafts [options]
|
|
689
784
|
|
|
690
|
-
|
|
691
|
-
already have a dedicated Step 5 operating record. Existing tests are
|
|
785
|
+
Preview or create missing draft External Evidence records for collection that
|
|
786
|
+
does not already have a dedicated Step 5 operating record. Existing tests are
|
|
787
|
+
preserved. Run after confirming applicable controls and source systems.
|
|
692
788
|
|
|
693
789
|
Options:
|
|
790
|
+
--preview Report proposed drafts without creating them
|
|
694
791
|
--json Print created and existing records as JSON
|
|
695
792
|
--root <path> Workspace path
|
|
696
793
|
--help Show this help`);
|
|
@@ -707,7 +804,7 @@ function agentOverview(model) {
|
|
|
707
804
|
help: "filegrc help",
|
|
708
805
|
version: "filegrc version",
|
|
709
806
|
serve: "filegrc serve [root]",
|
|
710
|
-
setup: "filegrc setup [setup.json|-] [--draft] [--json]",
|
|
807
|
+
setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
|
|
711
808
|
build: "filegrc build [root]",
|
|
712
809
|
validate: "filegrc validate [root] --json",
|
|
713
810
|
model: "filegrc model --json",
|
|
@@ -720,7 +817,7 @@ function agentOverview(model) {
|
|
|
720
817
|
search: "filegrc search <query> --json",
|
|
721
818
|
obligations: "filegrc obligations --json",
|
|
722
819
|
programReadiness: "filegrc program-readiness --json",
|
|
723
|
-
evidenceTestDrafts: "filegrc evidence-test-drafts --json",
|
|
820
|
+
evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
|
|
724
821
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
725
822
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
|
726
823
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
@@ -860,6 +957,46 @@ function printProgramPath(result) {
|
|
|
860
957
|
}
|
|
861
958
|
}
|
|
862
959
|
|
|
960
|
+
function summarizeProgramReadiness(result) {
|
|
961
|
+
const ownership = result.stages
|
|
962
|
+
.flatMap((stage) => stage.items)
|
|
963
|
+
.find((item) => item.id === "program-ownership");
|
|
964
|
+
const summarizeItem = (item) => item ? {
|
|
965
|
+
id: item.id,
|
|
966
|
+
status: item.status,
|
|
967
|
+
title: item.title,
|
|
968
|
+
message: item.message,
|
|
969
|
+
...(item.resourceType ? { resourceType: item.resourceType } : {}),
|
|
970
|
+
...(item.resourceId ? { resourceId: item.resourceId } : {}),
|
|
971
|
+
...(item.unresolvedAssignments?.length ? { unresolvedAssignments: item.unresolvedAssignments } : {})
|
|
972
|
+
} : null;
|
|
973
|
+
return {
|
|
974
|
+
schemaVersion: result.schemaVersion,
|
|
975
|
+
generatedAt: result.generatedAt,
|
|
976
|
+
asOf: result.asOf,
|
|
977
|
+
status: result.status,
|
|
978
|
+
evidenceReady: result.evidenceReady,
|
|
979
|
+
operating: result.operating,
|
|
980
|
+
canStartCandidatePeriod: result.canStartCandidatePeriod,
|
|
981
|
+
suggestedCandidatePeriodStart: result.suggestedCandidatePeriodStart,
|
|
982
|
+
target: result.target,
|
|
983
|
+
progress: result.progress,
|
|
984
|
+
counts: result.counts,
|
|
985
|
+
scopeCounts: Object.fromEntries(
|
|
986
|
+
Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
|
|
987
|
+
),
|
|
988
|
+
unresolvedOwnership: ownership?.unresolvedAssignments || [],
|
|
989
|
+
firstAction: summarizeItem(result.firstAction),
|
|
990
|
+
stages: result.stages.map((stage) => ({
|
|
991
|
+
id: stage.id,
|
|
992
|
+
title: stage.title,
|
|
993
|
+
status: stage.status,
|
|
994
|
+
counts: stage.counts,
|
|
995
|
+
firstAction: summarizeItem(stage.items.find(({ status }) => status === "action"))
|
|
996
|
+
}))
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
863
1000
|
function eventWindowText(window) {
|
|
864
1001
|
if (Number.isInteger(window?.endOffsetHours)) {
|
|
865
1002
|
return window.endOffsetHours === 0 ? "due at event time" : `due within ${window.endOffsetHours} hours`;
|
package/src/evidence-packet.js
CHANGED
|
@@ -245,7 +245,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
245
245
|
end,
|
|
246
246
|
timezone: loaded.workspace.timezone
|
|
247
247
|
});
|
|
248
|
-
const
|
|
248
|
+
const filegrcRecords = datedRecords.filter((record) => (
|
|
249
249
|
!NON_EVIDENCE_RECORD_TYPES.has(record.type)
|
|
250
250
|
&& controlIdsForRecord(byId.get(record.id), byId).size
|
|
251
251
|
));
|
|
@@ -319,7 +319,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
319
319
|
},
|
|
320
320
|
summary: {
|
|
321
321
|
datedRecords: datedRecords.length,
|
|
322
|
-
|
|
322
|
+
filegrcRecords: filegrcRecords.length,
|
|
323
323
|
records: packetRecords.length,
|
|
324
324
|
policies: policyIds.size,
|
|
325
325
|
controls: controlIds.size,
|
|
@@ -335,7 +335,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
335
335
|
warnings: warningCount
|
|
336
336
|
},
|
|
337
337
|
datedRecords: datedRecords.sort((a, b) => a.primaryDate.localeCompare(b.primaryDate) || byTitle(a, b)),
|
|
338
|
-
|
|
338
|
+
filegrcRecords: filegrcRecords.sort((a, b) => a.primaryDate.localeCompare(b.primaryDate) || byTitle(a, b)),
|
|
339
339
|
policies: [...policyIds].map((id) => recordSummary(byId.get(id))).filter(Boolean).sort(byTitle),
|
|
340
340
|
controls: [...controlIds].map((id) => recordSummary(byId.get(id))).filter(Boolean).sort(byTitle),
|
|
341
341
|
obligations,
|
|
@@ -557,7 +557,7 @@ async function writeChecksums(output, files) {
|
|
|
557
557
|
|
|
558
558
|
function controlMatrixCsv(packet) {
|
|
559
559
|
return csv([
|
|
560
|
-
["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "Frequency", "Operation Mode", "System IDs", "Requirement IDs", "Policy IDs", "Risk IDs", "
|
|
560
|
+
["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "Frequency", "Operation Mode", "System IDs", "Requirement IDs", "Policy IDs", "Risk IDs", "filegrc Evidence IDs", "External Evidence IDs", "Control Test IDs", "Test Outcomes", "Population IDs", "Population Counts", "Sample Sizes", "Exception Counts", "Population Evidence IDs", "Sample Evidence IDs"],
|
|
561
561
|
...packet.controlCoverage.map((control) => [
|
|
562
562
|
control.id,
|
|
563
563
|
control.code,
|
|
@@ -592,15 +592,15 @@ function packetHandlingMarkdown(packet) {
|
|
|
592
592
|
"",
|
|
593
593
|
`Evidence classifications: ${packet.handling.classifications.join(", ") || "none recorded"}`,
|
|
594
594
|
`External references present: ${packet.handling.containsExternalReferences ? "yes" : "no"}`,
|
|
595
|
-
"Encrypted by
|
|
595
|
+
"Encrypted by filegrc: no",
|
|
596
596
|
"",
|
|
597
597
|
"Review every included record and attachment for secrets, unnecessary personal data, customer data, and material outside the audit scope before transfer.",
|
|
598
598
|
"",
|
|
599
|
-
"Review `external-evidence-index.csv` before delivery. It identifies references that
|
|
599
|
+
"Review `external-evidence-index.csv` before delivery. It identifies references that filegrc did not copy. Reconcile those items to the auditor portal or other approved system so the engagement team can confirm it received the same evidence indexed here.",
|
|
600
600
|
"",
|
|
601
601
|
"Transfer this directory through the auditor's approved encrypted channel. Do not email an unencrypted packet. Give access only to the engagement team and retain or remove exported copies under the organization's evidence-retention rules.",
|
|
602
602
|
"",
|
|
603
|
-
"After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`.
|
|
603
|
+
"After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`. filegrc does not sign or encrypt the packet because those operations require organization-controlled keys and transfer-system choices.",
|
|
604
604
|
""
|
|
605
605
|
].join("\n");
|
|
606
606
|
}
|
|
@@ -972,7 +972,7 @@ function packetGaps({
|
|
|
972
972
|
if (controlNeedsExternalEvidence(control, model) && !coverage.evidenceIds.length) {
|
|
973
973
|
gaps.push(gap("error", "control-missing-external-evidence", `${coverage.code || coverage.title} relies on an external system but has no linked External Evidence in the packet.`, coverage.id));
|
|
974
974
|
} else if (!controlNeedsExternalEvidence(control, model) && !coverage.operatingRecordIds.length) {
|
|
975
|
-
gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated
|
|
975
|
+
gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated filegrc operating record in the packet.`, coverage.id));
|
|
976
976
|
}
|
|
977
977
|
if (audit?.auditKind !== "soc-2-type-1" && coverage.status === "implemented" && !coverage.operatingRecordIds.length && coverage.operationMode !== "automated") {
|
|
978
978
|
gaps.push(gap("warning", "control-missing-operating-record", `${coverage.code || coverage.title} has no dated operating record in the packet period.`, coverage.id));
|
|
@@ -1523,7 +1523,7 @@ function recordSummary(record) {
|
|
|
1523
1523
|
|
|
1524
1524
|
function packetMarkdown(packet) {
|
|
1525
1525
|
const readiness = packet.readiness.status === "delivery-ready"
|
|
1526
|
-
? "
|
|
1526
|
+
? "filegrc management checks passed. The engagement team still determines whether the evidence is sufficient and appropriate."
|
|
1527
1527
|
: `${packet.readiness.errors} errors and ${packet.readiness.warnings} warnings require review. This is a draft packet.`;
|
|
1528
1528
|
const periodLabel = packet.period.basis === "as-of"
|
|
1529
1529
|
? `as of ${packet.period.start}`
|
|
@@ -1543,7 +1543,7 @@ function packetMarkdown(packet) {
|
|
|
1543
1543
|
"",
|
|
1544
1544
|
"## Coverage",
|
|
1545
1545
|
"",
|
|
1546
|
-
`- ${packet.summary.
|
|
1546
|
+
`- ${packet.summary.filegrcRecords} filegrc Evidence records`,
|
|
1547
1547
|
`- ${packet.summary.obligationOccurrences} recurring obligation occurrences`,
|
|
1548
1548
|
`- ${packet.summary.eventRuns} event runs`,
|
|
1549
1549
|
`- ${packet.summary.evidence} External Evidence records`,
|
|
@@ -1554,7 +1554,7 @@ function packetMarkdown(packet) {
|
|
|
1554
1554
|
`- ${packet.summary.systems} in-scope systems`,
|
|
1555
1555
|
`- ${packet.summary.sourceSystems} cataloged source systems`,
|
|
1556
1556
|
"",
|
|
1557
|
-
"Open `index.html` for the auditor-oriented index. `control-matrix.csv` cross-references criteria, controls,
|
|
1557
|
+
"Open `index.html` for the auditor-oriented index. `control-matrix.csv` cross-references criteria, controls, filegrc Evidence, External Evidence, and tests. `source-system-index.csv` identifies the systems of record used to produce External Evidence. `external-evidence-index.csv` lists material that must be delivered or accessed outside this packet. For Type 2, `population-index.csv` records management's population reconciliation and fixed source exports. filegrc records, governed Markdown, fixed attachments, and committed historical versions are included in their respective directories.",
|
|
1558
1558
|
"",
|
|
1559
1559
|
"After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`. The checksum file covers every other packet file.",
|
|
1560
1560
|
""
|
|
@@ -1569,7 +1569,7 @@ function packetHtml(packet) {
|
|
|
1569
1569
|
: "<p>None.</p>";
|
|
1570
1570
|
const gaps = packet.gaps.length
|
|
1571
1571
|
? `<ul>${packet.gaps.map((item) => `<li class="${item.severity}"><strong>${escapeHtml(item.severity)}</strong> ${escapeHtml(item.message)}</li>`).join("")}</ul>`
|
|
1572
|
-
: "<p>
|
|
1572
|
+
: "<p>filegrc management checks passed. The engagement team still evaluates sufficiency and appropriateness.</p>";
|
|
1573
1573
|
const engagementDate = packet.period.basis === "as-of"
|
|
1574
1574
|
? `As of ${escapeHtml(packet.period.start)}`
|
|
1575
1575
|
: `${escapeHtml(packet.period.start)} through ${escapeHtml(packet.period.end)}`;
|
|
@@ -1592,25 +1592,25 @@ function packetHtml(packet) {
|
|
|
1592
1592
|
? packet.eventRuns.map((run) => `<article><h3><a href="records/obligation-event/${encodeURIComponent(run.id)}.json">${escapeHtml(run.title)}</a></h3><p>${escapeHtml(run.occurredAt || run.occurredOn)} · ${escapeHtml(run.status)} · ${run.completeCount} of ${run.actions.length} complete</p><table><thead><tr><th>Required action</th><th>Policy cutoff</th><th>Status</th></tr></thead><tbody>${run.actions.map((action) => `<tr><td><a href="records/action-item/${encodeURIComponent(action.actionItemId)}.json">${escapeHtml(action.title)}</a></td><td>${escapeHtml(action.dueWindowEndAt || action.dueWindowEnd)}</td><td>${escapeHtml(action.status)}</td></tr>`).join("")}</tbody></table></article>`).join("")
|
|
1593
1593
|
: "<p>No event workflows intersect this period.</p>";
|
|
1594
1594
|
const recordsById = new Map(packet.records.map((record) => [record.id, record]));
|
|
1595
|
-
const
|
|
1596
|
-
? `<table><thead><tr><th>Date</th><th>
|
|
1595
|
+
const filegrcRecords = packet.filegrcRecords.length
|
|
1596
|
+
? `<table><thead><tr><th>Date</th><th>filegrc record</th><th>Latest committed change</th></tr></thead><tbody>${packet.filegrcRecords.map((item) => {
|
|
1597
1597
|
const history = recordsById.get(item.id)?.history?.[0];
|
|
1598
1598
|
const source = history
|
|
1599
1599
|
? `${history.timestamp} · ${history.author} · ${history.subject}`
|
|
1600
1600
|
: "No committed file history";
|
|
1601
1601
|
return `<tr><td>${escapeHtml(item.primaryDate)}</td><td><a href="records/${encodeURIComponent(item.type)}/${encodeURIComponent(item.id)}.json">${escapeHtml(item.title)}</a><br><small>${escapeHtml(item.type)}</small></td><td>${escapeHtml(source)}</td></tr>`;
|
|
1602
1602
|
}).join("")}</tbody></table>`
|
|
1603
|
-
: "<p>No
|
|
1603
|
+
: "<p>No filegrc Evidence records matched this period.</p>";
|
|
1604
1604
|
const controlCoverage = packet.controlCoverage.length
|
|
1605
|
-
? `<p><a href="control-matrix.csv">Download control matrix CSV</a></p><table><thead><tr><th>Control</th><th>Status and scope</th><th>Criteria</th><th>
|
|
1605
|
+
? `<p><a href="control-matrix.csv">Download control matrix CSV</a></p><table><thead><tr><th>Control</th><th>Status and scope</th><th>Criteria</th><th>filegrc Evidence</th><th>External Evidence</th><th>Tests</th></tr></thead><tbody>${packet.controlCoverage.map((control) => `<tr><td><a href="records/control/${encodeURIComponent(control.id)}.json">${escapeHtml(control.code || control.id)}</a><small>${escapeHtml(control.title)}</small></td><td>${escapeHtml(control.status)}<small>${control.systemIds.map(escapeHtml).join(", ") || "No system scope"}</small></td><td>${control.requirementIds.map(escapeHtml).join("<br>") || "None"}</td><td>${control.operatingRecordIds.length}</td><td>${control.evidenceIds.length}</td><td>${control.tests.length}</td></tr>`).join("")}</tbody></table>`
|
|
1606
1606
|
: "<p>No controls were selected.</p>";
|
|
1607
|
-
const readinessLabel = packet.readiness.status === "delivery-ready" ? "
|
|
1607
|
+
const readinessLabel = packet.readiness.status === "delivery-ready" ? "filegrc management checks passed" : "Draft, do not deliver";
|
|
1608
1608
|
const packetDate = packet.period.basis === "as-of"
|
|
1609
1609
|
? `As of ${escapeHtml(packet.period.start)}`
|
|
1610
1610
|
: `${escapeHtml(packet.period.start)} through ${escapeHtml(packet.period.end)}`;
|
|
1611
1611
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Evidence packet</title><style>
|
|
1612
1612
|
body{font:14px/1.5 system-ui,sans-serif;color:#161825;max-width:1120px;margin:auto;padding:40px;background:#f7f8fc}header,section{background:#fff;border:1px solid #dfe3ef;border-radius:10px;padding:24px;margin:14px 0}h1,h2{margin-top:0}h1{font-size:26px}h2{font-size:17px}ul{padding-left:20px}li{margin:8px 0}small{display:block;color:#656c7e}.attachment{margin-right:10px;font-size:12px}.error{color:#8a2f28}.warning{color:#76500d}.readiness{display:inline-block;padding:5px 9px;border-radius:999px;background:#f7e4e2;color:#7a2520;font-weight:700}.readiness.ready{background:#e2f1e8;color:#245d3b}table{width:100%;border-collapse:collapse}th,td{padding:9px;border:1px solid #dfe3ef;text-align:left;vertical-align:top}code{overflow-wrap:anywhere}dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px}dt{font-weight:700}dd{margin:0}
|
|
1613
|
-
</style></head><body><header><p>SOC 2 evidence packet</p><span class="readiness ${packet.readiness.status === "delivery-ready" ? "ready" : ""}">${escapeHtml(readinessLabel)}</span><h1>${packetDate}</h1><p>${escapeHtml(packet.workspace.organizationName)} · revision <code>${escapeHtml(packet.revision.commit || "uncommitted")}</code></p></header>${section("Engagement scope", engagement)}${section("Review status", gaps)}${section("Control coverage", controlCoverage)}${section("Systems of record", sourceSystems)}${packet.period.basis === "period" ? section("Management population reconciliation", populations) : ""}${packet.period.basis === "period" ? section("Recurring obligation coverage", obligations) : ""}${packet.period.basis === "period" ? section("Event workflow coverage", eventRuns) : ""}${section("Policies", links(packet.policies))}${section("
|
|
1613
|
+
</style></head><body><header><p>SOC 2 evidence packet</p><span class="readiness ${packet.readiness.status === "delivery-ready" ? "ready" : ""}">${escapeHtml(readinessLabel)}</span><h1>${packetDate}</h1><p>${escapeHtml(packet.workspace.organizationName)} · revision <code>${escapeHtml(packet.revision.commit || "uncommitted")}</code></p></header>${section("Engagement scope", engagement)}${section("Review status", gaps)}${section("Control coverage", controlCoverage)}${section("Systems of record", sourceSystems)}${packet.period.basis === "period" ? section("Management population reconciliation", populations) : ""}${packet.period.basis === "period" ? section("Recurring obligation coverage", obligations) : ""}${packet.period.basis === "period" ? section("Event workflow coverage", eventRuns) : ""}${section("Policies", links(packet.policies))}${section("filegrc Evidence", filegrcRecords)}${section("External Evidence", evidence)}${section("Integrity and history", "<p>Verify all transferred files with <code>SHA256SUMS</code>. Committed prior versions are under <code>history/</code> with an index that records their source paths and Git metadata.</p>")}</body></html>`;
|
|
1614
1614
|
}
|
|
1615
1615
|
|
|
1616
1616
|
async function writePacketFile(output, relativePath, source, files) {
|
package/src/git.js
CHANGED
|
@@ -227,7 +227,7 @@ async function pushWorkspaceUnlocked(root) {
|
|
|
227
227
|
|
|
228
228
|
function syncReadySummary(root, action) {
|
|
229
229
|
const summary = getGitSummary(root);
|
|
230
|
-
if (!summary.available) throw new Error(`Git history is unavailable for this workspace, so
|
|
230
|
+
if (!summary.available) throw new Error(`Git history is unavailable for this workspace, so filegrc cannot ${action}.`);
|
|
231
231
|
if (!summary.branch) throw new Error(`Check out a branch before trying to ${action}.`);
|
|
232
232
|
if (!summary.clean) throw new Error(`Commit or discard workspace changes before trying to ${action}.`);
|
|
233
233
|
return summary;
|
package/src/index.js
CHANGED
|
@@ -50,8 +50,8 @@ export {
|
|
|
50
50
|
nextCalendarOccurrence
|
|
51
51
|
} from "./recurrence.js";
|
|
52
52
|
export { searchResources, searchableValues } from "./search.js";
|
|
53
|
-
export {
|
|
54
|
-
export { normalizeSetupPayload, setupWorkspace } from "./setup.js";
|
|
53
|
+
export { createFilegrcServer, serveWorkspace } from "./server.js";
|
|
54
|
+
export { normalizeSetupPayload, planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
55
55
|
export { createAppState } from "./state.js";
|
|
56
56
|
export { currentCalendarDate, formatCalendarDate, formatLocalDateTime } from "./time.js";
|
|
57
57
|
export { validateWorkspace } from "./validate.js";
|
package/src/model-docs.js
CHANGED
|
@@ -67,7 +67,7 @@ export function generateModelDocumentation(model) {
|
|
|
67
67
|
"",
|
|
68
68
|
...model.evidenceSourceFamilies.map((item) => (
|
|
69
69
|
item.collectionTestRequired === false
|
|
70
|
-
? `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description}
|
|
70
|
+
? `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} filegrc operating records: ${item.operationRecordTypes.map((type) => `\`${type}\``).join(", ")}. No separate collection test is required. ${item.timing}`
|
|
71
71
|
: `- **${item.title}** (${item.sourceKinds.map((kind) => `\`${kind}\``).join(", ")}): ${item.description} Test external collection: ${item.testPrompt} ${item.timing}`
|
|
72
72
|
)),
|
|
73
73
|
"",
|
package/src/parties.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const CURRENT_PERSON_STATUSES = new Set(["active", "external"]);
|
|
2
|
+
const CURRENT_TEAM_STATUSES = new Set(["active"]);
|
|
3
|
+
|
|
4
|
+
export function partyPeople(ids = [], byId, options = {}, seen = new Set()) {
|
|
5
|
+
const people = new Set();
|
|
6
|
+
for (const id of ids) {
|
|
7
|
+
if (seen.has(id)) continue;
|
|
8
|
+
seen.add(id);
|
|
9
|
+
const record = byId.get(id);
|
|
10
|
+
if (
|
|
11
|
+
record?.type === "person"
|
|
12
|
+
&& (!options.personStatuses || options.personStatuses.has(record.status))
|
|
13
|
+
) {
|
|
14
|
+
people.add(id);
|
|
15
|
+
}
|
|
16
|
+
if (
|
|
17
|
+
record?.type === "team"
|
|
18
|
+
&& (!options.teamStatuses || options.teamStatuses.has(record.status))
|
|
19
|
+
) {
|
|
20
|
+
for (const personId of partyPeople(
|
|
21
|
+
[...(record.memberIds || []), ...(record.chairIds || [])],
|
|
22
|
+
byId,
|
|
23
|
+
options,
|
|
24
|
+
seen
|
|
25
|
+
)) {
|
|
26
|
+
people.add(personId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return people;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function currentPartyPeople(ids = [], byId) {
|
|
34
|
+
return partyPeople(ids, byId, {
|
|
35
|
+
personStatuses: CURRENT_PERSON_STATUSES,
|
|
36
|
+
teamStatuses: CURRENT_TEAM_STATUSES
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function partiesIndependent(ownerIds = [], approverIds = [], byId) {
|
|
41
|
+
const owners = partyPeople(ownerIds, byId);
|
|
42
|
+
const approvers = partyPeople(approverIds, byId);
|
|
43
|
+
return owners.size > 0
|
|
44
|
+
&& approvers.size > 0
|
|
45
|
+
&& ![...owners].some((id) => approvers.has(id));
|
|
46
|
+
}
|
package/src/paths.js
CHANGED
|
@@ -15,7 +15,7 @@ export function resolveWorkspaceRoot(input = process.cwd()) {
|
|
|
15
15
|
current = parent;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
throw new Error("No
|
|
18
|
+
throw new Error("No filegrc workspace was found from the requested path.");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function isWithin(parent, candidate) {
|
package/src/program-lifecycle.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { currentPartyPeople } from "./parties.js";
|
|
2
|
+
|
|
1
3
|
export function obligationProgramStatus(obligation, byId, asOf) {
|
|
4
|
+
if (currentPartyPeople(obligation.ownerIds || [], byId).size === 0) return "proposed";
|
|
2
5
|
const policyIds = obligation.policyIds || [];
|
|
3
6
|
const policiesReady = policyIds.every((id) => {
|
|
4
7
|
const policy = byId.get(id);
|