filegrc 0.7.1 → 0.8.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 +6 -6
- package/model/index.js +21 -4
- package/model/v5.json +10233 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +182 -45
- package/src/audit-transition.js +3 -2
- package/src/batch-review.js +7 -6
- package/src/cli.js +74 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +145 -0
- package/src/evidence-packet.js +199 -78
- package/src/external-reviewer.js +5 -4
- package/src/files.js +141 -32
- package/src/git.js +71 -7
- package/src/index.js +4 -1
- package/src/model-migration.js +218 -7
- package/src/obligations.js +14 -11
- package/src/policy-library.js +2 -1
- package/src/program-lifecycle.js +93 -3
- package/src/program-path.js +13 -8
- package/src/program-readiness.js +235 -71
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +29 -7
- package/src/setup.js +8 -5
- package/src/soc2.js +3 -2
- package/src/validate.js +148 -14
- package/src/web.js +160 -15
- package/src/workflow.js +18 -4
- package/src/workspace.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "filegrc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Zero-dependency Git-native GRC engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"src"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node
|
|
24
|
+
"test": "node ../../scripts/run-filegrc-tests.mjs",
|
|
25
|
+
"test:fast": "node ../../scripts/run-filegrc-tests.mjs --fast",
|
|
26
|
+
"test:full": "npm test"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
27
29
|
"node": ">=20"
|
package/src/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modelSupports } from "../model/index.js";
|
|
1
2
|
import { createResourceId } from "./id.js";
|
|
2
3
|
import { markdownEntries } from "./resource-markdown.js";
|
|
3
4
|
import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
|
|
@@ -116,7 +117,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
116
117
|
recommendedMarkdown.length
|
|
117
118
|
? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
|
|
118
119
|
: "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
|
|
119
|
-
...(type === "program" &&
|
|
120
|
+
...(type === "program" && modelSupports(loaded.model, "program-scope")
|
|
120
121
|
? ["Review Requirement applicability with npx filegrc review-applicability --type requirement --scaffold, then preview and apply the reviewed decisions as one validated batch."]
|
|
121
122
|
: []),
|
|
122
123
|
"Run npx filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
|
|
@@ -125,7 +126,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
125
126
|
"Required and status-dependent fields are complete, and the lifecycle status matches the facts.",
|
|
126
127
|
"Every relationship resolves to the intended existing record.",
|
|
127
128
|
"Dates describe the business event in the workspace time zone, not the file edit time.",
|
|
128
|
-
...(type === "program" &&
|
|
129
|
+
...(type === "program" && modelSupports(loaded.model, "program-scope")
|
|
129
130
|
? ["Every selected Requirement has an applicable or not-applicable decision reviewed against the current Program scope."]
|
|
130
131
|
: []),
|
|
131
132
|
...(recommendedMarkdown.length
|
|
@@ -217,7 +218,7 @@ function applyModelScaffoldDefaults(record, loaded, options = {}) {
|
|
|
217
218
|
}[program?.assuranceGoal];
|
|
218
219
|
if (kind) record.auditKind = kind;
|
|
219
220
|
for (const field of ["frameworkIds", "systemIds", "requirementIds", "controlIds"]) {
|
|
220
|
-
if (field === "requirementIds" &&
|
|
221
|
+
if (field === "requirementIds" && modelSupports(loaded.model, "program-scope")) {
|
|
221
222
|
record[field] = (program.requirementApplicability || []).filter(({ decision }) => decision === "applicable").map(({ requirementId }) => requirementId);
|
|
222
223
|
} else if (program?.[field]?.length) record[field] = [...program[field]];
|
|
223
224
|
}
|
package/src/audit-preparation.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { modelSupports } from "../model/index.js";
|
|
4
|
+
import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
|
|
3
5
|
import {
|
|
4
6
|
coverageContains,
|
|
5
7
|
coverageEnd,
|
|
@@ -15,6 +17,7 @@ import { currentPartyPeople, partiesIndependent } from "./parties.js";
|
|
|
15
17
|
import { resolveDataPath } from "./paths.js";
|
|
16
18
|
import { assessProgramReadiness } from "./program-readiness.js";
|
|
17
19
|
import { markdownEntries } from "./resource-markdown.js";
|
|
20
|
+
import { contentRevisionBindingsMatch, governedDocumentIsOperating } from "./program-lifecycle.js";
|
|
18
21
|
import {
|
|
19
22
|
auditorWasEngaged,
|
|
20
23
|
missingSoc2References,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
signatoryAppointmentIssue,
|
|
26
29
|
subsequentEventsReviewIssue
|
|
27
30
|
} from "./soc2.js";
|
|
31
|
+
import { currentCalendarDate } from "./time.js";
|
|
28
32
|
import { loadWorkspace } from "./workspace.js";
|
|
29
33
|
|
|
30
34
|
const NON_EVIDENCE_RECORD_TYPES = new Set([
|
|
@@ -65,9 +69,13 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
65
69
|
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
66
70
|
|
|
67
71
|
const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
|
|
72
|
+
asOf: options.asOf,
|
|
68
73
|
generatedAt: options.generatedAt,
|
|
69
74
|
programId: audit?.programId
|
|
70
75
|
});
|
|
76
|
+
const documentActivations = audit && modelSupports(loaded.model, "governed-document-activation")
|
|
77
|
+
? await auditDocumentActivationAssessments(loaded, audit, byId, programReadiness.asOf)
|
|
78
|
+
: [];
|
|
71
79
|
const stages = [
|
|
72
80
|
programFoundationStage(programReadiness, loaded.workspace),
|
|
73
81
|
engagementStage(audit, byId, programReadiness),
|
|
@@ -75,7 +83,7 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
75
83
|
];
|
|
76
84
|
const fieldworkSections = audit
|
|
77
85
|
? [
|
|
78
|
-
await documentsStage(loaded, audit, byId),
|
|
86
|
+
await documentsStage(loaded, audit, byId, programReadiness.asOf),
|
|
79
87
|
evidenceStage(audit, records, byId, loaded.model),
|
|
80
88
|
populationsStage(audit, records, byId, loaded.model)
|
|
81
89
|
]
|
|
@@ -107,10 +115,23 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
107
115
|
&& coverageStart(audit.coverage)
|
|
108
116
|
&& coverageEnd(audit.coverage)
|
|
109
117
|
&& initializationNeeded(audit, records, loaded.model)),
|
|
118
|
+
documentActivations,
|
|
110
119
|
stages
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
export async function assessAuditDocumentActivations(input = process.cwd(), options = {}) {
|
|
124
|
+
const loaded = input?.resources && input?.model && input?.entries
|
|
125
|
+
? input
|
|
126
|
+
: await loadWorkspace(input);
|
|
127
|
+
if (!modelSupports(loaded.model, "governed-document-activation")) return [];
|
|
128
|
+
const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
|
|
129
|
+
if (!audit) throw new Error(`Audit "${options.auditId || ""}" was not found.`);
|
|
130
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
131
|
+
const asOf = options.asOf || currentCalendarDate(loaded.workspace.timezone);
|
|
132
|
+
return auditDocumentActivationAssessments(loaded, audit, byId, asOf);
|
|
133
|
+
}
|
|
134
|
+
|
|
114
135
|
export async function prepareAuditWorkspace(input, options = {}) {
|
|
115
136
|
const loaded = await loadWorkspace(input);
|
|
116
137
|
const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
|
|
@@ -168,7 +189,7 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
168
189
|
const selectedControls = (audit.controlIds || [])
|
|
169
190
|
.map((id) => loaded.resources.find((record) => record.id === id))
|
|
170
191
|
.filter(Boolean);
|
|
171
|
-
const v4 =
|
|
192
|
+
const v4 = modelSupports(loaded.model, "program-scope");
|
|
172
193
|
const sourceSystems = loaded.resources.filter((record) => record.type === (v4 ? "component" : "system"));
|
|
173
194
|
const populations = (audit.auditKind === "soc-2-type-2" ? model.populationTemplates || [] : [])
|
|
174
195
|
.filter((template) => !existingKinds.has(template.kind))
|
|
@@ -269,7 +290,7 @@ function scopeStage(loaded, audit, records, byId, programReadiness) {
|
|
|
269
290
|
));
|
|
270
291
|
}
|
|
271
292
|
|
|
272
|
-
const v4 =
|
|
293
|
+
const v4 = modelSupports(programReadiness.dataModelVersion, "program-scope");
|
|
273
294
|
const engagementStart = coverageStart(audit.coverage);
|
|
274
295
|
const engagementEnd = coverageEnd(audit.coverage);
|
|
275
296
|
const scopeRevision = assessScopeRevision(loaded, audit, v4);
|
|
@@ -744,9 +765,12 @@ function engagementStage(audit, byId, programReadiness) {
|
|
|
744
765
|
const engagementTermsComplete = Boolean(
|
|
745
766
|
engagementTerms?.type === "document"
|
|
746
767
|
&& engagementTerms.documentKind === "soc2-engagement-terms"
|
|
747
|
-
&&
|
|
768
|
+
&& governedDocumentIsOperating(
|
|
769
|
+
engagementTerms,
|
|
770
|
+
programReadiness.asOf,
|
|
771
|
+
{ modelVersion: programReadiness.dataModelVersion }
|
|
772
|
+
)
|
|
748
773
|
&& engagementTerms.approvedOn
|
|
749
|
-
&& engagementTerms.effectiveOn
|
|
750
774
|
);
|
|
751
775
|
const acknowledgementPeople = (audit.managementAcknowledgedByIds || [])
|
|
752
776
|
.map((id) => byId.get(id))
|
|
@@ -805,7 +829,7 @@ function engagementStage(audit, byId, programReadiness) {
|
|
|
805
829
|
}
|
|
806
830
|
)
|
|
807
831
|
];
|
|
808
|
-
if (
|
|
832
|
+
if (modelSupports(programReadiness.dataModelVersion, "guided-workflow")) {
|
|
809
833
|
items.push(
|
|
810
834
|
item(
|
|
811
835
|
"engagement-terms",
|
|
@@ -853,51 +877,160 @@ function fieldworkStage(audit, sections) {
|
|
|
853
877
|
);
|
|
854
878
|
}
|
|
855
879
|
|
|
856
|
-
async function
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
880
|
+
async function auditDocumentActivationAssessments(loaded, audit, byId, asOf) {
|
|
881
|
+
const links = new Map();
|
|
882
|
+
const addLink = (documentId, role, definition = null) => {
|
|
883
|
+
if (!documentId) return;
|
|
884
|
+
const current = links.get(documentId) || { documentId, roles: [], definitions: [] };
|
|
885
|
+
current.roles.push(role);
|
|
886
|
+
if (definition) current.definitions.push(definition);
|
|
887
|
+
links.set(documentId, current);
|
|
888
|
+
};
|
|
889
|
+
addLink(audit.engagementTermsDocumentId, "engagement-terms");
|
|
890
|
+
for (const definition of applicableManagementDocuments(audit, loaded.model.auditReadiness || {})) {
|
|
891
|
+
addLink(audit[definition.field], definition.field, definition);
|
|
892
|
+
}
|
|
893
|
+
for (const documentId of audit.supplementalDocumentIds || []) addLink(documentId, "supplemental");
|
|
894
|
+
|
|
895
|
+
const assessments = [];
|
|
896
|
+
for (const link of links.values()) {
|
|
897
|
+
const document = byId.get(link.documentId);
|
|
898
|
+
if (document?.type !== "document") continue;
|
|
899
|
+
if (link.roles.every((role) => role === "supplemental") && document.workflowScope !== "engagement") continue;
|
|
900
|
+
const source = await primaryMarkdown(loaded, document);
|
|
901
|
+
const issues = [];
|
|
902
|
+
if (document.workflowScope !== "engagement") issues.push("Set workflowScope to engagement.");
|
|
903
|
+
if (document.template === true) issues.push("Replace the starter template with the completed engagement Document.");
|
|
904
|
+
if (!substantiveMarkdown(source)) issues.push("Complete the Document Markdown.");
|
|
905
|
+
const placeholders = openPlaceholderCount(source);
|
|
906
|
+
if (placeholders) issues.push(`Resolve ${placeholders} open ${placeholders === 1 ? "placeholder" : "placeholders"}.`);
|
|
907
|
+
if (link.roles.includes("engagement-terms") && document.documentKind !== "soc2-engagement-terms") {
|
|
908
|
+
issues.push("Use documentKind soc2-engagement-terms for the accepted engagement terms.");
|
|
866
909
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
870
|
-
.map((record) => record.reconciledOn)
|
|
871
|
-
.filter(Boolean)
|
|
872
|
-
.sort()
|
|
873
|
-
.at(-1);
|
|
874
|
-
if (latestReconciliation && document.approvedOn < latestReconciliation) {
|
|
875
|
-
contentIssues.push("Approve the period completeness statement after the last population reconciliation.");
|
|
876
|
-
}
|
|
910
|
+
for (const definition of link.definitions) {
|
|
911
|
+
issues.push(...await managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId));
|
|
877
912
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
913
|
+
const approvalComplete = Boolean(
|
|
914
|
+
["approved", "active"].includes(document.status)
|
|
915
|
+
&& document.approvedOn
|
|
916
|
+
&& document.approvedContentRevisions
|
|
917
|
+
&& (document.activationBasis === "legacy-v4"
|
|
918
|
+
? (document.ownerIds || []).length
|
|
919
|
+
: currentPartyPeople(document.ownerIds || [], byId).size)
|
|
920
|
+
&& (document.approverIds || []).length
|
|
921
|
+
&& partiesIndependent(document.ownerIds, document.approverIds, byId)
|
|
922
|
+
);
|
|
923
|
+
if (!approvalComplete) issues.push("Complete the independent approval and bind the approved Markdown revision first.");
|
|
924
|
+
if (document.status === "active" && document.activationBasis !== "legacy-v4") {
|
|
925
|
+
if (document.activationBasis !== "recorded") issues.push("Record the Step 5 activation basis.");
|
|
926
|
+
if (!document.activatedOn) issues.push("Record the separate Step 5 activation date.");
|
|
927
|
+
else if (document.activatedOn > asOf) issues.push(`The activation date ${document.activatedOn} is after ${asOf}.`);
|
|
928
|
+
if (!(document.activatedByIds || []).length) issues.push("Name the active Person who performed activation.");
|
|
889
929
|
else {
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
930
|
+
const invalidActorIds = document.activatedByIds.filter((id) => !personWasActiveOn(byId.get(id), document.activatedOn));
|
|
931
|
+
if (invalidActorIds.length) issues.push(`Activation actors were not active on ${document.activatedOn}: ${invalidActorIds.join(", ")}.`);
|
|
892
932
|
}
|
|
933
|
+
if (!document.activatedContentRevisions) issues.push("Bind activation to the exact Document Markdown revision.");
|
|
934
|
+
else if (!contentRevisionBindingsMatch(document.approvedContentRevisions, document.activatedContentRevisions)) {
|
|
935
|
+
issues.push("The activated revision must match the unchanged approved revision.");
|
|
936
|
+
}
|
|
937
|
+
if (!document.effectiveOn) issues.push("Record the effective date.");
|
|
938
|
+
else if (document.effectiveOn > asOf) issues.push(`The Document does not become effective until ${document.effectiveOn}.`);
|
|
939
|
+
}
|
|
940
|
+
const operating = governedDocumentIsOperating(document, asOf, loaded.model) && issues.length === 0;
|
|
941
|
+
const state = operating
|
|
942
|
+
? "active-and-operating"
|
|
943
|
+
: document.status === "active"
|
|
944
|
+
? "active-with-gaps"
|
|
945
|
+
: document.status === "approved" && issues.length === 0
|
|
946
|
+
? "ready-to-activate"
|
|
947
|
+
: document.status === "approved"
|
|
948
|
+
? "approved-not-ready"
|
|
949
|
+
: "approval-pending";
|
|
950
|
+
assessments.push({
|
|
951
|
+
auditId: audit.id,
|
|
952
|
+
documentId: document.id,
|
|
953
|
+
title: document.title,
|
|
954
|
+
roles: [...new Set(link.roles)],
|
|
955
|
+
state,
|
|
956
|
+
label: auditDocumentActivationLabel(state),
|
|
957
|
+
issues: [...new Set(issues)],
|
|
958
|
+
approvedOn: document.approvedOn || null,
|
|
959
|
+
activatedOn: document.activatedOn || null,
|
|
960
|
+
activatedByIds: document.activatedByIds || [],
|
|
961
|
+
effectiveOn: document.effectiveOn || null,
|
|
962
|
+
approvalRevisionBound: Boolean(document.approvedContentRevisions),
|
|
963
|
+
activationRevisionBound: Boolean(document.activatedContentRevisions),
|
|
964
|
+
gapCount: [...new Set(issues)].length
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return assessments.sort((left, right) => left.title.localeCompare(right.title));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function auditDocumentActivationLabel(state) {
|
|
971
|
+
return ({
|
|
972
|
+
"approval-pending": "Approval pending in Step 5",
|
|
973
|
+
"approved-not-ready": "Approved, engagement facts incomplete",
|
|
974
|
+
"ready-to-activate": "Ready to activate in Step 5",
|
|
975
|
+
"active-with-gaps": "Active with lifecycle or engagement gaps",
|
|
976
|
+
"active-and-operating": "Active and ready for the engagement"
|
|
977
|
+
})[state] || state;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId) {
|
|
981
|
+
const issues = managementDocumentContentIssues(source, definition, audit);
|
|
982
|
+
if (document.documentKind !== definition.kind) {
|
|
983
|
+
issues.push(`Use documentKind ${definition.kind} for this Audit field.`);
|
|
984
|
+
}
|
|
985
|
+
const engagementEnd = coverageEnd(audit?.coverage);
|
|
986
|
+
if (document.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
|
|
987
|
+
issues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
|
|
988
|
+
}
|
|
989
|
+
if (definition.kind === "soc2-period-completeness" && document.approvedOn) {
|
|
990
|
+
const latestReconciliation = loaded.resources
|
|
991
|
+
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
992
|
+
.map((record) => record.reconciledOn)
|
|
993
|
+
.filter(Boolean)
|
|
994
|
+
.sort()
|
|
995
|
+
.at(-1);
|
|
996
|
+
if (latestReconciliation && document.approvedOn < latestReconciliation) {
|
|
997
|
+
issues.push("Approve the period completeness statement after the last population reconciliation.");
|
|
893
998
|
}
|
|
999
|
+
}
|
|
1000
|
+
if (definition.kind === "soc2-management-representation") {
|
|
1001
|
+
const signedEvidence = (document.evidenceIds || [])
|
|
1002
|
+
.map((id) => byId.get(id))
|
|
1003
|
+
.find((record) => (
|
|
1004
|
+
record?.type === "evidence"
|
|
1005
|
+
&& record.status === "verified"
|
|
1006
|
+
&& record.artifactKind === "signed-record"
|
|
1007
|
+
&& record.artifactSubtype === "signed-management-representation"
|
|
1008
|
+
&& (record.filePaths || []).length
|
|
1009
|
+
));
|
|
1010
|
+
if (!signedEvidence) issues.push("Link verified signed-record Evidence with subtype signed-management-representation and a fixed-format copy of the signed letter.");
|
|
1011
|
+
else {
|
|
1012
|
+
const dateIssue = signedRepresentationDateIssue(signedEvidence, audit, loaded.model.modelVersion);
|
|
1013
|
+
if (dateIssue) issues.push(dateIssue);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return issues;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function documentsStage(loaded, audit, byId, asOf) {
|
|
1020
|
+
const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
|
|
1021
|
+
const items = [];
|
|
1022
|
+
for (const definition of definitions) {
|
|
1023
|
+
const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
|
|
1024
|
+
const source = document ? await primaryMarkdown(loaded, document) : "";
|
|
1025
|
+
const contentIssues = document
|
|
1026
|
+
? await managementDocumentLifecycleIssues(loaded, audit, definition, document, source, asOf, byId)
|
|
1027
|
+
: [];
|
|
894
1028
|
const complete = Boolean(
|
|
895
1029
|
document
|
|
896
1030
|
&& document.type === "document"
|
|
897
1031
|
&& document.template !== true
|
|
898
|
-
&& document
|
|
1032
|
+
&& governedDocumentIsOperating(document, asOf, loaded.model)
|
|
899
1033
|
&& document.approvedOn
|
|
900
|
-
&& document.effectiveOn
|
|
901
1034
|
&& (document.ownerIds || []).length
|
|
902
1035
|
&& (document.approverIds || []).length
|
|
903
1036
|
&& partiesIndependent(document.ownerIds, document.approverIds, byId)
|
|
@@ -912,7 +1045,11 @@ async function documentsStage(loaded, audit, byId) {
|
|
|
912
1045
|
complete ? "complete" : representationLater ? "later" : "action",
|
|
913
1046
|
definition.title,
|
|
914
1047
|
complete
|
|
915
|
-
?
|
|
1048
|
+
? document.activationBasis === "legacy-v4"
|
|
1049
|
+
? "Linked Markdown is complete and effective. Its active state is preserved from model v4, which did not record approval and activation as separate events."
|
|
1050
|
+
: modelSupports(loaded.model, "governed-document-activation")
|
|
1051
|
+
? "Linked Markdown is complete, independently approved, separately activated by a named Person, revision-bound at both events, and effective."
|
|
1052
|
+
: "Linked Markdown is complete, active, approved, and effective."
|
|
916
1053
|
: document
|
|
917
1054
|
? `${definition.timing} ${contentIssues[0] || "Complete and approve the engagement-specific document."}`
|
|
918
1055
|
: `Link the starter ${definition.title.toLowerCase()} to this audit. ${definition.timing}`,
|
|
@@ -925,7 +1062,7 @@ async function documentsStage(loaded, audit, byId) {
|
|
|
925
1062
|
export function signedRepresentationDateIssue(evidence, audit, modelVersion) {
|
|
926
1063
|
const engagementEnd = coverageEnd(audit?.coverage);
|
|
927
1064
|
const timingMessage = `The signed representation must be dated on or after the engagement ${audit?.auditKind === "soc-2-type-1" ? "date" : "period end"}.`;
|
|
928
|
-
if (
|
|
1065
|
+
if (!modelSupports(modelVersion, "program-scope")) {
|
|
929
1066
|
return !evidence?.collectedOn || (engagementEnd && evidence.collectedOn < engagementEnd)
|
|
930
1067
|
? timingMessage
|
|
931
1068
|
: null;
|
|
@@ -999,7 +1136,7 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
999
1136
|
externalEvidence[0] || { type: "evidence" }
|
|
1000
1137
|
)
|
|
1001
1138
|
];
|
|
1002
|
-
const v4 =
|
|
1139
|
+
const v4 = modelSupports(model, "program-scope");
|
|
1003
1140
|
const systems = records.filter((record) => record.type === (v4 ? "component" : "system") && record.status === "active");
|
|
1004
1141
|
const sourceId = (record) => v4 ? record.sourceComponentId : record.sourceSystemId;
|
|
1005
1142
|
for (const source of model.evidenceSourceFamilies || []) {
|
|
@@ -1103,7 +1240,7 @@ function auditorStage(audit, byId, modelVersion) {
|
|
|
1103
1240
|
const signatoryStatus = !audit || ["planned", "in-progress", "fieldwork"].includes(audit.status)
|
|
1104
1241
|
? "later"
|
|
1105
1242
|
: signatoryIssue ? "action" : "complete";
|
|
1106
|
-
const managementItems =
|
|
1243
|
+
const managementItems = modelSupports(modelVersion, "program-scope") ? [
|
|
1107
1244
|
item(
|
|
1108
1245
|
"subsequent-events",
|
|
1109
1246
|
subsequentEventsStatus,
|
package/src/audit-transition.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { modelSupports } from "../model/index.js";
|
|
1
2
|
import { createResource } from "./files.js";
|
|
2
3
|
import { createResourceId } from "./id.js";
|
|
3
4
|
import { loadWorkspace } from "./workspace.js";
|
|
4
5
|
|
|
5
6
|
export async function planNextAuditCycle(input = process.cwd(), options = {}) {
|
|
6
7
|
const loaded = await loadWorkspace(input);
|
|
7
|
-
if (!
|
|
8
|
-
throw new Error("Audit-cycle carry-forward requires a model v3 or
|
|
8
|
+
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
9
|
+
throw new Error("Audit-cycle carry-forward requires a model v3 or newer workspace.");
|
|
9
10
|
}
|
|
10
11
|
const prior = loaded.resources.find((record) => (
|
|
11
12
|
record.type === "audit" && record.id === options.priorAuditId
|
package/src/batch-review.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { modelSupports } from "../model/index.js";
|
|
3
4
|
import { applyResourceBatch } from "./files.js";
|
|
4
5
|
import { getWorkspaceRevisionSnapshot } from "./git.js";
|
|
5
6
|
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
@@ -20,8 +21,8 @@ const REVIEWABLE_TYPES = new Set([
|
|
|
20
21
|
export async function scaffoldApplicabilityReview(input = process.cwd(), options = {}) {
|
|
21
22
|
const context = await applicabilityReviewContext(input);
|
|
22
23
|
const { loaded } = context;
|
|
23
|
-
if (!
|
|
24
|
-
throw new Error("Batch applicability review requires a model v3 or
|
|
24
|
+
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
25
|
+
throw new Error("Batch applicability review requires a model v3 or newer workspace.");
|
|
25
26
|
}
|
|
26
27
|
const requestedType = options.type ? String(options.type) : null;
|
|
27
28
|
if (requestedType && !REVIEWABLE_TYPES.has(requestedType)) {
|
|
@@ -34,7 +35,7 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
|
|
|
34
35
|
const records = loaded.resources.filter((record) => (
|
|
35
36
|
REVIEWABLE_TYPES.has(record.type)
|
|
36
37
|
&& (!requestedType || record.type === requestedType)
|
|
37
|
-
&& (record.type === "requirement" &&
|
|
38
|
+
&& (record.type === "requirement" && modelSupports(loaded.model, "program-scope")
|
|
38
39
|
? !reviewedRequirementIds.has(record.id)
|
|
39
40
|
: !record.applicabilityReview)
|
|
40
41
|
&& !["retired", "superseded"].includes(record.status)
|
|
@@ -64,8 +65,8 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
|
|
|
64
65
|
|
|
65
66
|
function planApplicabilityReviewWithContext(context, options) {
|
|
66
67
|
const { basis, loaded } = context;
|
|
67
|
-
if (!
|
|
68
|
-
throw new Error("Batch applicability review requires a model v3 or
|
|
68
|
+
if (!modelSupports(loaded.model, "guided-workflow")) {
|
|
69
|
+
throw new Error("Batch applicability review requires a model v3 or newer workspace.");
|
|
69
70
|
}
|
|
70
71
|
if (!Array.isArray(options.decisions) || !options.decisions.length) {
|
|
71
72
|
throw new Error("Applicability review needs at least one decision.");
|
|
@@ -112,7 +113,7 @@ function planApplicabilityReviewWithContext(context, options) {
|
|
|
112
113
|
if (!["applicable", "not-applicable"].includes(result)) {
|
|
113
114
|
throw new Error(`Requirement "${record.id}" must be applicable or not-applicable.`);
|
|
114
115
|
}
|
|
115
|
-
if (
|
|
116
|
+
if (modelSupports(loaded.model, "program-scope")) {
|
|
116
117
|
v4RequirementDecisions.push({
|
|
117
118
|
requirementId: record.id,
|
|
118
119
|
decision: result,
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { loadModel } from "../model/index.js";
|
|
4
|
+
import { ACTIVE_MODEL_VERSION, loadModel, SUPPORTED_MODEL_VERSIONS } from "../model/index.js";
|
|
5
5
|
import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
|
|
6
6
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
7
7
|
import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
scaffoldCollectionReview
|
|
17
17
|
} from "./collection-review.js";
|
|
18
18
|
import { buildWorkspace } from "./build.js";
|
|
19
|
+
import { activateDocuments, planDocumentActivation, scaffoldDocumentActivation } from "./document-activation.js";
|
|
19
20
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
20
21
|
import {
|
|
21
22
|
addEvidenceAttachment,
|
|
@@ -183,15 +184,16 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
183
184
|
}
|
|
184
185
|
if (command === "migrate") {
|
|
185
186
|
const targetModel = String(flags["to-model"] || "");
|
|
186
|
-
if (!["2", "3", "4"].includes(targetModel)) throw new Error("Pass --to-model 2, --to-model 3, or --to-model
|
|
187
|
-
const
|
|
187
|
+
if (!["2", "3", "4", "5"].includes(targetModel)) throw new Error("Pass --to-model 2, --to-model 3, --to-model 4, or --to-model 5.");
|
|
188
|
+
const migrationDecisions = flags.decisions
|
|
188
189
|
? JSON.parse(await readFile(resolve(String(flags.decisions)), "utf8"))
|
|
189
190
|
: undefined;
|
|
190
191
|
const options = {
|
|
191
192
|
jobTitle: flags["job-title"],
|
|
192
193
|
startsOn: flags["starts-on"],
|
|
193
194
|
targetModelVersion: targetModel,
|
|
194
|
-
systemDecisions:
|
|
195
|
+
systemDecisions: migrationDecisions?.systemDecisions || (targetModel === "4" ? migrationDecisions : undefined),
|
|
196
|
+
documentScopes: migrationDecisions?.documentScopes || (targetModel === "5" ? migrationDecisions : undefined)
|
|
195
197
|
};
|
|
196
198
|
const plan = await planModelMigration(root, options);
|
|
197
199
|
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
@@ -617,6 +619,31 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
617
619
|
else console.log(`Activated ${result.policyIds.length} Policies effective ${result.effectiveOn}.`);
|
|
618
620
|
return result;
|
|
619
621
|
}
|
|
622
|
+
if (command === "activate-documents") {
|
|
623
|
+
if (flags.scaffold) {
|
|
624
|
+
const result = await scaffoldDocumentActivation(root, { programId: flags.program, auditId: flags.audit });
|
|
625
|
+
console.log(JSON.stringify(result, null, 2));
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
const payload = await readSetupPayload(positionals[0]);
|
|
629
|
+
const options = {
|
|
630
|
+
...payload,
|
|
631
|
+
documentIds: flags.document ? String(flags.document).split(",").filter(Boolean) : payload.documentIds,
|
|
632
|
+
activatedByIds: flags["activated-by"] ? String(flags["activated-by"]).split(",").filter(Boolean) : payload.activatedByIds,
|
|
633
|
+
activatedOn: flags["activated-on"] || payload.activatedOn,
|
|
634
|
+
effectiveOn: flags["effective-on"] || payload.effectiveOn,
|
|
635
|
+
programId: flags.program || payload.programId,
|
|
636
|
+
auditId: flags.audit || payload.auditId,
|
|
637
|
+
confirmed: flags.yes === true
|
|
638
|
+
};
|
|
639
|
+
const result = flags.preview
|
|
640
|
+
? await planDocumentActivation(root, options)
|
|
641
|
+
: await withWorkflowDelta(root, () => activateDocuments(root, options));
|
|
642
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
643
|
+
else if (flags.preview) console.log(`Document activation preview: ${result.documentIds.length} governed Documents effective ${result.effectiveOn}.`);
|
|
644
|
+
else console.log(`Activated ${result.documentIds.length} governed Documents effective ${result.effectiveOn}.`);
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
620
647
|
if (command === "policy-library") {
|
|
621
648
|
if (flags.yes && !flags.accept) {
|
|
622
649
|
throw new Error("Pass --accept <proposal-id> with --yes after reviewing the policy-library diff.");
|
|
@@ -1055,7 +1082,7 @@ Usage:
|
|
|
1055
1082
|
filegrc build [root] [--output .filegrc/site]
|
|
1056
1083
|
filegrc validate [root] [--json]
|
|
1057
1084
|
filegrc model [--json|--write-docs|--check-docs]
|
|
1058
|
-
filegrc migrate --to-model <2|3|4> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
1085
|
+
filegrc migrate --to-model <2|3|4|5> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
1059
1086
|
filegrc describe <resource-type>
|
|
1060
1087
|
filegrc types [--json]
|
|
1061
1088
|
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
@@ -1078,6 +1105,7 @@ Usage:
|
|
|
1078
1105
|
filegrc review-applicability [--scaffold --type requirement|control|commitment|complementary-control] [decisions.json|-] [--preview|--yes] [--json]
|
|
1079
1106
|
filegrc review-collection <resource-type> [--scaffold | review.json|-] [--preview|--yes] [--json]
|
|
1080
1107
|
filegrc activate-policies [--scaffold | activation.json|-] [--effective-on YYYY-MM-DD] [--preview|--yes] [--json]
|
|
1108
|
+
filegrc activate-documents [--scaffold | activation.json|-] [--program id | --audit id] [--activated-by person-id] [--activated-on YYYY-MM-DD] [--effective-on YYYY-MM-DD] [--preview|--yes] [--json]
|
|
1081
1109
|
filegrc policy-library [--json | --accept proposal-id --proposal-revision revision --yes]
|
|
1082
1110
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--risk-level normal|high] [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
1083
1111
|
filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
@@ -1145,19 +1173,20 @@ Options:
|
|
|
1145
1173
|
}
|
|
1146
1174
|
if (command === "migrate") {
|
|
1147
1175
|
console.log(`Usage:
|
|
1148
|
-
filegrc migrate --to-model
|
|
1176
|
+
filegrc migrate --to-model <${SUPPORTED_MODEL_VERSIONS.join("|")}> [options]
|
|
1149
1177
|
|
|
1150
1178
|
Upgrade a workspace through an explicit, reviewable model boundary. Model v1
|
|
1151
|
-
workspaces migrate to v2 first.
|
|
1179
|
+
workspaces migrate to v2 first. Continue one version at a time through v${ACTIVE_MODEL_VERSION}. v4 separates
|
|
1152
1180
|
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1153
|
-
specific Assets, Vendors, normalized information, and Evidence Artifacts.
|
|
1181
|
+
specific Assets, Vendors, normalized information, and Evidence Artifacts. Model v5 separates
|
|
1182
|
+
Document approval from activation and records program-versus-engagement scope. v3 migration
|
|
1154
1183
|
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1155
1184
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1156
1185
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
1157
1186
|
|
|
1158
1187
|
Options:
|
|
1159
|
-
--to-model <version> Required target model; migrations must run in order through
|
|
1160
|
-
--decisions <path>
|
|
1188
|
+
--to-model <version> Required target model; migrations must run in order through ${SUPPORTED_MODEL_VERSIONS.join(", ")}
|
|
1189
|
+
--decisions <path> JSON systemDecisions for v4 or documentScopes for ambiguous v5 Documents
|
|
1161
1190
|
--preview Show the complete atomic record plan without writing
|
|
1162
1191
|
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
1163
1192
|
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
@@ -1167,7 +1196,7 @@ Options:
|
|
|
1167
1196
|
--help Show this help
|
|
1168
1197
|
|
|
1169
1198
|
Start with:
|
|
1170
|
-
npx filegrc migrate --to-model
|
|
1199
|
+
npx filegrc migrate --to-model ${ACTIVE_MODEL_VERSION} --preview --json`);
|
|
1171
1200
|
return;
|
|
1172
1201
|
}
|
|
1173
1202
|
if (command === "program-readiness") {
|
|
@@ -1248,6 +1277,30 @@ Options:
|
|
|
1248
1277
|
--help Show this help`);
|
|
1249
1278
|
return;
|
|
1250
1279
|
}
|
|
1280
|
+
if (command === "activate-documents") {
|
|
1281
|
+
console.log(`Usage:
|
|
1282
|
+
filegrc activate-documents --scaffold [--program id | --audit id]
|
|
1283
|
+
filegrc activate-documents <activation.json|-> [--audit id] [--activated-by person-id] [--activated-on YYYY-MM-DD] [--effective-on YYYY-MM-DD] [--preview|--yes] [--json]
|
|
1284
|
+
|
|
1285
|
+
Activate required governed plans and schedules in Step 3 after their linked
|
|
1286
|
+
Controls are implemented, or activate engagement Documents in Step 5 after
|
|
1287
|
+
their audit-specific facts are complete. Approval, activation, and effective
|
|
1288
|
+
dates remain separate, and activation binds its own exact Markdown revision.
|
|
1289
|
+
|
|
1290
|
+
Options:
|
|
1291
|
+
--scaffold Print the ready activation payload without writing
|
|
1292
|
+
--program <id> Program to assess when more than one active Program exists
|
|
1293
|
+
--audit <id> Audit whose engagement Documents should be activated in Step 5
|
|
1294
|
+
--activated-by <id> Active Person who performs the activation
|
|
1295
|
+
--activated-on <date> Actual activation date, which must be today
|
|
1296
|
+
--effective-on <date> Effective date on or after activation
|
|
1297
|
+
--preview Validate and show the atomic updates without writing
|
|
1298
|
+
--yes Confirm and apply the reviewed activation
|
|
1299
|
+
--json Print the result as JSON
|
|
1300
|
+
--root <path> Workspace path
|
|
1301
|
+
--help Show this help`);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1251
1304
|
if (command === "policy-library") {
|
|
1252
1305
|
console.log(`Usage:
|
|
1253
1306
|
filegrc policy-library [--json]
|
|
@@ -1294,7 +1347,7 @@ function agentOverview(model) {
|
|
|
1294
1347
|
build: "filegrc build [root]",
|
|
1295
1348
|
validate: "filegrc validate [root] --json",
|
|
1296
1349
|
model: "filegrc model --json",
|
|
1297
|
-
migrate: "filegrc migrate --to-model
|
|
1350
|
+
migrate: "filegrc migrate --to-model 5 --preview --json",
|
|
1298
1351
|
describe: "filegrc describe <resource-type>",
|
|
1299
1352
|
types: "filegrc types --json",
|
|
1300
1353
|
guide: "filegrc guide [resource-type] --json",
|
|
@@ -1313,6 +1366,7 @@ function agentOverview(model) {
|
|
|
1313
1366
|
reconcile: "filegrc reconcile --preview --json",
|
|
1314
1367
|
externalReviewerSetup: "filegrc external-reviewer-setup [--scaffold | <reviewer.json|-> --preview] --json",
|
|
1315
1368
|
policyActivation: "filegrc activate-policies [--scaffold | <activation.json|-> --preview] --json",
|
|
1369
|
+
documentActivation: "filegrc activate-documents [--scaffold | <activation.json|-> --preview] --json",
|
|
1316
1370
|
nextAuditCycle: "filegrc next-audit-cycle <prior-audit-id> --start <date> --end <date> --preview --json",
|
|
1317
1371
|
reviewApplicability: "filegrc review-applicability <decisions.json|-> --preview --json",
|
|
1318
1372
|
reviewCollection: "filegrc review-collection <resource-type> [--scaffold | <review.json|-> --preview] --json",
|
|
@@ -1465,6 +1519,7 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1465
1519
|
evidenceReady: readiness.evidenceReady,
|
|
1466
1520
|
operating: readiness.operating,
|
|
1467
1521
|
policyActivations: readiness.policyActivations,
|
|
1522
|
+
documentActivations: readiness.documentActivations,
|
|
1468
1523
|
policyLibraryProposals: readiness.policyLibraryProposals,
|
|
1469
1524
|
stages
|
|
1470
1525
|
};
|
|
@@ -1668,6 +1723,13 @@ function summarizeProgramReadiness(result) {
|
|
|
1668
1723
|
label,
|
|
1669
1724
|
gapCount
|
|
1670
1725
|
})),
|
|
1726
|
+
documentActivations: result.documentActivations.map(({ documentId, title, state, label, gapCount }) => ({
|
|
1727
|
+
documentId,
|
|
1728
|
+
title,
|
|
1729
|
+
state,
|
|
1730
|
+
label,
|
|
1731
|
+
gapCount
|
|
1732
|
+
})),
|
|
1671
1733
|
policyLibraryProposals: result.policyLibraryProposals,
|
|
1672
1734
|
unresolvedOwnership: {
|
|
1673
1735
|
count: unresolvedOwnership.length,
|