filegrc 0.5.0 → 0.6.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 +14 -6
- package/model/index.js +4 -4
- package/model/v4.json +10052 -0
- package/package.json +2 -2
- package/src/agent.js +17 -5
- package/src/audit-preparation.js +17 -13
- package/src/audit-transition.js +7 -4
- package/src/batch-review.js +31 -8
- package/src/cli.js +43 -25
- package/src/collection-review.js +71 -25
- package/src/evidence-packet.js +76 -43
- package/src/external-reviewer.js +4 -4
- package/src/files.js +70 -6
- package/src/git.js +70 -13
- package/src/index.js +1 -0
- package/src/model-migration.js +631 -14
- package/src/obligations.js +14 -7
- package/src/program-path.js +49 -38
- package/src/program-readiness.js +94 -54
- package/src/program.js +52 -0
- package/src/reconciliation.js +2 -2
- package/src/server.js +50 -8
- package/src/setup.js +56 -21
- package/src/source-coverage.js +13 -9
- package/src/startup.js +1 -1
- package/src/state.js +17 -14
- package/src/timing.js +6 -0
- package/src/validate.js +100 -9
- package/src/web.js +709 -151
- package/src/workflow.js +47 -25
package/src/workflow.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./obligations.js";
|
|
21
21
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
22
22
|
import { assessProgramReadiness } from "./program-readiness.js";
|
|
23
|
+
import { resolveProgram } from "./program.js";
|
|
23
24
|
import { planReconciliation } from "./reconciliation.js";
|
|
24
25
|
import { currentCalendarDate } from "./time.js";
|
|
25
26
|
import { validateWorkspace } from "./validate.js";
|
|
@@ -81,14 +82,22 @@ export async function assessWorkflow(input, options = {}) {
|
|
|
81
82
|
: await loadWorkspace(input);
|
|
82
83
|
const workspace = loaded.workspace
|
|
83
84
|
|| loaded.resources.find((record) => record.type === "workspace");
|
|
85
|
+
const selectedAuditRecords = selectedAudits(loaded.resources, options.auditId);
|
|
86
|
+
const programRecord = resolveProgram(loaded, options.programId || (options.auditId ? selectedAuditRecords[0]?.programId : undefined));
|
|
87
|
+
if (options.auditId && selectedAuditRecords[0]?.programId && selectedAuditRecords[0].programId !== programRecord.id) {
|
|
88
|
+
throw new Error(`Audit "${options.auditId}" belongs to Program "${selectedAuditRecords[0].programId}", not "${programRecord.id}".`);
|
|
89
|
+
}
|
|
90
|
+
const audits = String(loaded.model.modelVersion) === "4"
|
|
91
|
+
? selectedAuditRecords.filter(({ programId }) => programId === programRecord.id)
|
|
92
|
+
: selectedAuditRecords;
|
|
84
93
|
const timezone = options.timezone || workspace?.timezone || "UTC";
|
|
85
94
|
const asOf = options.asOf || currentCalendarDate(timezone);
|
|
86
95
|
const evaluatedAt = options.evaluatedAt || options.now || new Date().toISOString();
|
|
87
96
|
const program = options.programReadiness || await assessProgramReadiness(loaded, {
|
|
88
97
|
asOf,
|
|
89
|
-
generatedAt: evaluatedAt
|
|
98
|
+
generatedAt: evaluatedAt,
|
|
99
|
+
programId: programRecord.type === "program" ? programRecord.id : undefined
|
|
90
100
|
});
|
|
91
|
-
const audits = selectedAudits(loaded.resources, options.auditId);
|
|
92
101
|
const auditPreparations = options.auditPreparations || Object.fromEntries(await Promise.all(
|
|
93
102
|
audits.map(async (audit) => [
|
|
94
103
|
audit.id,
|
|
@@ -111,26 +120,27 @@ export async function assessWorkflow(input, options = {}) {
|
|
|
111
120
|
const coverage = normalizeCoverage(
|
|
112
121
|
options.coverage
|
|
113
122
|
|| (options.auditId ? audits[0]?.coverage : null)
|
|
114
|
-
||
|
|
123
|
+
|| programRecord?.candidateCoverage
|
|
115
124
|
);
|
|
116
125
|
const periodFindings = await assessPeriodHealth(loaded, {
|
|
117
126
|
coverage,
|
|
118
127
|
asOf,
|
|
119
128
|
evaluatedAt,
|
|
120
|
-
|
|
129
|
+
programId: programRecord.type === "program" ? programRecord.id : undefined,
|
|
130
|
+
controlIds: options.auditId ? audits[0]?.controlIds : programRecord?.controlIds
|
|
121
131
|
});
|
|
122
132
|
const guidedFindings = [
|
|
123
133
|
...programFindings(program),
|
|
124
134
|
...Object.values(auditPreparations).flatMap(auditFindings),
|
|
125
135
|
...appointmentTemplateFindings(loaded),
|
|
126
|
-
...sourceCoverageFindings(loaded),
|
|
136
|
+
...sourceCoverageFindings(loaded, programRecord),
|
|
127
137
|
...reconciliationFindings(reconciliation),
|
|
128
138
|
...periodFindings,
|
|
129
|
-
...auditLifecycleFindings(loaded, audits)
|
|
139
|
+
...auditLifecycleFindings(loaded, audits, programRecord)
|
|
130
140
|
];
|
|
131
141
|
const findings = [
|
|
132
142
|
...validationFindings(validation, loaded),
|
|
133
|
-
...removeRedundantRecordFindings(recordFinalizationFindings(loaded), guidedFindings),
|
|
143
|
+
...removeRedundantRecordFindings(recordFinalizationFindings(loaded, programRecord), guidedFindings),
|
|
134
144
|
...guidedFindings
|
|
135
145
|
];
|
|
136
146
|
const workItems = buildWorkItems(loaded.resources, obligationPlan, {
|
|
@@ -329,11 +339,11 @@ function validationFindings(validation, loaded) {
|
|
|
329
339
|
});
|
|
330
340
|
}
|
|
331
341
|
|
|
332
|
-
function recordFinalizationFindings(loaded) {
|
|
342
|
+
function recordFinalizationFindings(loaded, program) {
|
|
333
343
|
const findings = [];
|
|
334
344
|
for (const record of loaded.resources) {
|
|
335
345
|
if (["workspace", "renderer-settings"].includes(record.type)) continue;
|
|
336
|
-
const incomplete = recordIncompleteReason(record, loaded);
|
|
346
|
+
const incomplete = recordIncompleteReason(record, loaded, program);
|
|
337
347
|
if (incomplete) {
|
|
338
348
|
findings.push(finalizationFinding(record, incomplete));
|
|
339
349
|
}
|
|
@@ -389,7 +399,7 @@ function subjectKey(subject) {
|
|
|
389
399
|
return `${subject.type}:${subject.id}`;
|
|
390
400
|
}
|
|
391
401
|
|
|
392
|
-
function recordIncompleteReason(record, loaded) {
|
|
402
|
+
function recordIncompleteReason(record, loaded, program) {
|
|
393
403
|
if (record.type === "requirement" && record.applicability === "undetermined") {
|
|
394
404
|
return {
|
|
395
405
|
state: "ready",
|
|
@@ -418,7 +428,7 @@ function recordIncompleteReason(record, loaded) {
|
|
|
418
428
|
"partially-implemented": "Finish the remaining control design, operation, source, and scheduling work."
|
|
419
429
|
};
|
|
420
430
|
if (!messages[record.status]) return null;
|
|
421
|
-
const requiredness = recordFinalizationRequiredness(record, loaded);
|
|
431
|
+
const requiredness = recordFinalizationRequiredness(record, loaded, program);
|
|
422
432
|
if (record.status === "blocked") {
|
|
423
433
|
const byId = new Map(loaded.resources.map((resource) => [resource.id, resource]));
|
|
424
434
|
return {
|
|
@@ -437,11 +447,12 @@ function recordIncompleteReason(record, loaded) {
|
|
|
437
447
|
};
|
|
438
448
|
}
|
|
439
449
|
|
|
440
|
-
function recordFinalizationRequiredness(record, loaded) {
|
|
450
|
+
function recordFinalizationRequiredness(record, loaded, program) {
|
|
441
451
|
if (!["policy", "document", "training"].includes(record.type)) return "required";
|
|
452
|
+
const target = program || resolveProgram(loaded);
|
|
442
453
|
const selectedControlIds = new Set(
|
|
443
|
-
|
|
444
|
-
?
|
|
454
|
+
target?.controlIds?.length
|
|
455
|
+
? target.controlIds
|
|
445
456
|
: loaded.resources
|
|
446
457
|
.filter((resource) => resource.type === "control" && !["not-applicable", "retired"].includes(resource.status))
|
|
447
458
|
.map(({ id }) => id)
|
|
@@ -606,11 +617,19 @@ function appointmentFinding(kind, template, record, state, requiredness) {
|
|
|
606
617
|
};
|
|
607
618
|
}
|
|
608
619
|
|
|
609
|
-
function sourceCoverageFindings(loaded) {
|
|
620
|
+
function sourceCoverageFindings(loaded, program) {
|
|
621
|
+
const selected = String(loaded.model.modelVersion) === "4"
|
|
622
|
+
? new Set(program?.controlIds || [])
|
|
623
|
+
: null;
|
|
610
624
|
const selectedControlIds = loaded.resources
|
|
611
|
-
.filter((record) =>
|
|
625
|
+
.filter((record) => (
|
|
626
|
+
record.type === "control"
|
|
627
|
+
&& record.status !== "not-applicable"
|
|
628
|
+
&& record.status !== "retired"
|
|
629
|
+
&& (!selected || selected.has(record.id))
|
|
630
|
+
))
|
|
612
631
|
.map(({ id }) => id);
|
|
613
|
-
return assessSourceCoverageReadiness(loaded, selectedControlIds)
|
|
632
|
+
return assessSourceCoverageReadiness(loaded, selectedControlIds, program)
|
|
614
633
|
.map(({ family, record, complete }) => {
|
|
615
634
|
const state = complete ? "complete" : "ready";
|
|
616
635
|
const code = `evidence-source.${family.id}.coverage`;
|
|
@@ -659,15 +678,16 @@ function reconciliationFindings(reconciliation) {
|
|
|
659
678
|
|
|
660
679
|
async function assessPeriodHealth(loaded, options) {
|
|
661
680
|
const coverage = options.coverage;
|
|
681
|
+
const target = resolveProgram(loaded, options.programId);
|
|
662
682
|
if (!coverage?.start || !coverage?.end) {
|
|
663
|
-
if (
|
|
683
|
+
if (target?.assuranceGoal !== "soc-2-type-2") return [];
|
|
664
684
|
return [periodFinding(
|
|
665
685
|
"period.coverage.select",
|
|
666
686
|
"Select the candidate operating period",
|
|
667
687
|
"Set the candidate Type 2 start and end dates before FileGRC can calculate continuous policy, control, source, role, and obligation coverage.",
|
|
668
688
|
"ready",
|
|
669
|
-
{ type:
|
|
670
|
-
[{ kind: "command", command:
|
|
689
|
+
{ type: target.type, id: target.id },
|
|
690
|
+
[{ kind: "command", command: `npx filegrc get ${target.id} --mutation` }]
|
|
671
691
|
)];
|
|
672
692
|
}
|
|
673
693
|
|
|
@@ -680,7 +700,8 @@ async function assessPeriodHealth(loaded, options) {
|
|
|
680
700
|
"obligation",
|
|
681
701
|
"policy",
|
|
682
702
|
"source-coverage",
|
|
683
|
-
"system"
|
|
703
|
+
"system",
|
|
704
|
+
"component"
|
|
684
705
|
].includes(record.type));
|
|
685
706
|
const paths = relevantEntries.map(({ relativePath }) => `data/${relativePath}`);
|
|
686
707
|
const histories = getWorkspaceHistories(loaded.root, paths, 50);
|
|
@@ -843,8 +864,9 @@ async function assessPeriodHealth(loaded, options) {
|
|
|
843
864
|
return findings;
|
|
844
865
|
}
|
|
845
866
|
|
|
846
|
-
function auditLifecycleFindings(loaded, audits) {
|
|
847
|
-
if (String(loaded.model.modelVersion)
|
|
867
|
+
function auditLifecycleFindings(loaded, audits, program) {
|
|
868
|
+
if (!["3", "4"].includes(String(loaded.model.modelVersion))) return [];
|
|
869
|
+
const target = program || resolveProgram(loaded);
|
|
848
870
|
const findings = [];
|
|
849
871
|
for (const audit of audits) {
|
|
850
872
|
if (!(audit.controlIds || []).length) {
|
|
@@ -857,7 +879,7 @@ function auditLifecycleFindings(loaded, audits) {
|
|
|
857
879
|
));
|
|
858
880
|
}
|
|
859
881
|
if (!audit.scopeRevision) {
|
|
860
|
-
const workspaceControls = new Set(
|
|
882
|
+
const workspaceControls = new Set(target?.controlIds || []);
|
|
861
883
|
const omittedControls = [...workspaceControls].filter((id) => !(audit.controlIds || []).includes(id));
|
|
862
884
|
findings.push(auditLifecycleFinding(
|
|
863
885
|
audit,
|
|
@@ -879,7 +901,7 @@ function auditLifecycleFindings(loaded, audits) {
|
|
|
879
901
|
"Set the exact start and end dates before evaluating expected occurrences and source continuity."
|
|
880
902
|
));
|
|
881
903
|
}
|
|
882
|
-
const candidateStart = coverageStart(
|
|
904
|
+
const candidateStart = coverageStart(target?.candidateCoverage);
|
|
883
905
|
if (start && candidateStart && start < candidateStart) {
|
|
884
906
|
findings.push(auditLifecycleFinding(
|
|
885
907
|
audit,
|