filegrc 0.7.0 → 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 +14 -6
- package/model/index.js +21 -4
- package/model/v4.json +97 -6
- package/model/v5.json +10233 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +635 -81
- package/src/audit-transition.js +8 -2
- package/src/batch-review.js +21 -11
- package/src/cli.js +117 -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 +393 -106
- package/src/external-reviewer.js +5 -4
- package/src/files.js +194 -35
- package/src/git.js +106 -10
- package/src/index.js +10 -1
- package/src/model-migration.js +218 -7
- package/src/obligations.js +14 -11
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +827 -0
- package/src/program-lifecycle.js +93 -3
- package/src/program-path.js +14 -9
- package/src/program-readiness.js +306 -75
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +29 -7
- package/src/setup.js +25 -5
- package/src/soc2.js +228 -0
- package/src/state.js +8 -0
- package/src/validate.js +168 -14
- package/src/web.js +257 -35
- package/src/workflow.js +107 -44
- package/src/workspace.js +5 -0
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
|
|
@@ -37,6 +38,8 @@ export async function planNextAuditCycle(input = process.cwd(), options = {}) {
|
|
|
37
38
|
subserviceVendorIds: [...(prior.subserviceVendorIds || [])],
|
|
38
39
|
...(prior.subserviceMethod ? { subserviceMethod: prior.subserviceMethod } : {})
|
|
39
40
|
}),
|
|
41
|
+
...(prior.subserviceConclusion ? { subserviceConclusion: prior.subserviceConclusion } : {}),
|
|
42
|
+
...(prior.subserviceConclusionRationale ? { subserviceConclusionRationale: prior.subserviceConclusionRationale } : {}),
|
|
40
43
|
...(prior.complementaryControlsConclusion
|
|
41
44
|
? { complementaryControlsConclusion: prior.complementaryControlsConclusion }
|
|
42
45
|
: {}),
|
|
@@ -59,6 +62,9 @@ export async function planNextAuditCycle(input = process.cwd(), options = {}) {
|
|
|
59
62
|
"requirementIds",
|
|
60
63
|
"controlIds",
|
|
61
64
|
"complementaryControlIds",
|
|
65
|
+
"subserviceConclusion",
|
|
66
|
+
"subserviceConclusionRationale",
|
|
67
|
+
"subserviceTreatments",
|
|
62
68
|
"subserviceVendorIds",
|
|
63
69
|
"subserviceMethod",
|
|
64
70
|
"auditorVendorId",
|
package/src/batch-review.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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";
|
|
6
7
|
import { resolveDataPath } from "./paths.js";
|
|
7
8
|
import { resolveProgram } from "./program.js";
|
|
8
9
|
import { markdownEntries } from "./resource-markdown.js";
|
|
10
|
+
import { soc2RequirementApplicabilityConstraint } from "./soc2.js";
|
|
9
11
|
import { assessWorkflow, buildWorkflowDelta } from "./workflow.js";
|
|
10
12
|
import { loadWorkspace } from "./workspace.js";
|
|
11
13
|
|
|
@@ -19,8 +21,8 @@ const REVIEWABLE_TYPES = new Set([
|
|
|
19
21
|
export async function scaffoldApplicabilityReview(input = process.cwd(), options = {}) {
|
|
20
22
|
const context = await applicabilityReviewContext(input);
|
|
21
23
|
const { loaded } = context;
|
|
22
|
-
if (!
|
|
23
|
-
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.");
|
|
24
26
|
}
|
|
25
27
|
const requestedType = options.type ? String(options.type) : null;
|
|
26
28
|
if (requestedType && !REVIEWABLE_TYPES.has(requestedType)) {
|
|
@@ -33,7 +35,7 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
|
|
|
33
35
|
const records = loaded.resources.filter((record) => (
|
|
34
36
|
REVIEWABLE_TYPES.has(record.type)
|
|
35
37
|
&& (!requestedType || record.type === requestedType)
|
|
36
|
-
&& (record.type === "requirement" &&
|
|
38
|
+
&& (record.type === "requirement" && modelSupports(loaded.model, "program-scope")
|
|
37
39
|
? !reviewedRequirementIds.has(record.id)
|
|
38
40
|
: !record.applicabilityReview)
|
|
39
41
|
&& !["retired", "superseded"].includes(record.status)
|
|
@@ -44,11 +46,15 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
|
|
|
44
46
|
reviewedOn: null,
|
|
45
47
|
decisions: records
|
|
46
48
|
.sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
|
|
47
|
-
.map((record) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
.map((record) => {
|
|
50
|
+
const constraint = soc2RequirementApplicabilityConstraint(record, program, loaded.model.modelVersion);
|
|
51
|
+
return {
|
|
52
|
+
id: record.id,
|
|
53
|
+
decision: constraint?.requiredDecision || null,
|
|
54
|
+
rationale: constraint?.defaultRationale || null,
|
|
55
|
+
...(constraint ? { constraint } : {})
|
|
56
|
+
};
|
|
57
|
+
})
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -59,8 +65,8 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
|
|
|
59
65
|
|
|
60
66
|
function planApplicabilityReviewWithContext(context, options) {
|
|
61
67
|
const { basis, loaded } = context;
|
|
62
|
-
if (!
|
|
63
|
-
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.");
|
|
64
70
|
}
|
|
65
71
|
if (!Array.isArray(options.decisions) || !options.decisions.length) {
|
|
66
72
|
throw new Error("Applicability review needs at least one decision.");
|
|
@@ -89,6 +95,10 @@ function planApplicabilityReviewWithContext(context, options) {
|
|
|
89
95
|
if (!reviewedByIds.length || !reviewedOn || !rationale) {
|
|
90
96
|
throw new Error(`Decision for "${record.id}" needs a reviewer, review date, and rationale.`);
|
|
91
97
|
}
|
|
98
|
+
const constraint = soc2RequirementApplicabilityConstraint(record, program, loaded.model.modelVersion);
|
|
99
|
+
if (constraint && !constraint.allowedDecisions.includes(result)) {
|
|
100
|
+
throw new Error(`${record.reference || record.title} must be applicable because it is required for the selected SOC 2 Security program.`);
|
|
101
|
+
}
|
|
92
102
|
const next = {
|
|
93
103
|
...record,
|
|
94
104
|
applicabilityReview: {
|
|
@@ -103,7 +113,7 @@ function planApplicabilityReviewWithContext(context, options) {
|
|
|
103
113
|
if (!["applicable", "not-applicable"].includes(result)) {
|
|
104
114
|
throw new Error(`Requirement "${record.id}" must be applicable or not-applicable.`);
|
|
105
115
|
}
|
|
106
|
-
if (
|
|
116
|
+
if (modelSupports(loaded.model, "program-scope")) {
|
|
107
117
|
v4RequirementDecisions.push({
|
|
108
118
|
requirementId: record.id,
|
|
109
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,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
} from "./external-reviewer.js";
|
|
43
44
|
import { relativeToWorkspace, resolveDataPath } from "./paths.js";
|
|
44
45
|
import { activatePolicies, planPolicyActivation, scaffoldPolicyActivation } from "./policy-activation.js";
|
|
46
|
+
import { applyPolicyLibraryUpgrade, assessPolicyLibraryUpgrades } from "./policy-library.js";
|
|
45
47
|
import { buildAgentProgramPath } from "./program-path.js";
|
|
46
48
|
import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
47
49
|
import { resolveProgram } from "./program.js";
|
|
@@ -182,15 +184,16 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
182
184
|
}
|
|
183
185
|
if (command === "migrate") {
|
|
184
186
|
const targetModel = String(flags["to-model"] || "");
|
|
185
|
-
if (!["2", "3", "4"].includes(targetModel)) throw new Error("Pass --to-model 2, --to-model 3, or --to-model
|
|
186
|
-
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
|
|
187
189
|
? JSON.parse(await readFile(resolve(String(flags.decisions)), "utf8"))
|
|
188
190
|
: undefined;
|
|
189
191
|
const options = {
|
|
190
192
|
jobTitle: flags["job-title"],
|
|
191
193
|
startsOn: flags["starts-on"],
|
|
192
194
|
targetModelVersion: targetModel,
|
|
193
|
-
systemDecisions:
|
|
195
|
+
systemDecisions: migrationDecisions?.systemDecisions || (targetModel === "4" ? migrationDecisions : undefined),
|
|
196
|
+
documentScopes: migrationDecisions?.documentScopes || (targetModel === "5" ? migrationDecisions : undefined)
|
|
194
197
|
};
|
|
195
198
|
const plan = await planModelMigration(root, options);
|
|
196
199
|
if (!flags.preview && plan.sourceModelVersion !== plan.targetModelVersion && !flags.yes) {
|
|
@@ -616,6 +619,54 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
616
619
|
else console.log(`Activated ${result.policyIds.length} Policies effective ${result.effectiveOn}.`);
|
|
617
620
|
return result;
|
|
618
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
|
+
}
|
|
647
|
+
if (command === "policy-library") {
|
|
648
|
+
if (flags.yes && !flags.accept) {
|
|
649
|
+
throw new Error("Pass --accept <proposal-id> with --yes after reviewing the policy-library diff.");
|
|
650
|
+
}
|
|
651
|
+
const result = flags.accept
|
|
652
|
+
? await withWorkflowDelta(root, () => applyPolicyLibraryUpgrade(root, String(flags.accept), {
|
|
653
|
+
confirmed: flags.yes === true,
|
|
654
|
+
proposalRevision: flags["proposal-revision"] ? String(flags["proposal-revision"]) : null
|
|
655
|
+
}))
|
|
656
|
+
: await assessPolicyLibraryUpgrades(root);
|
|
657
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
658
|
+
else if (flags.accept) console.log(`Accepted policy-library proposal ${flags.accept}.`);
|
|
659
|
+
else if (!result.proposals.length) console.log("No starter policy-library update applies. Existing content is unchanged.");
|
|
660
|
+
else {
|
|
661
|
+
for (const proposal of result.proposals) {
|
|
662
|
+
console.log(`${proposal.title} (${proposal.id})`);
|
|
663
|
+
console.log(proposal.message);
|
|
664
|
+
for (const change of proposal.changes) console.log(`\n${change.diff}`);
|
|
665
|
+
console.log(`\nAccept with: filegrc policy-library --accept ${proposal.id} --proposal-revision ${proposal.revision} --yes`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
619
670
|
if (command === "trigger") {
|
|
620
671
|
const result = await withWorkflowDelta(root, () => createObligationEvent(root, {
|
|
621
672
|
eventType: positionals[0],
|
|
@@ -1031,7 +1082,7 @@ Usage:
|
|
|
1031
1082
|
filegrc build [root] [--output .filegrc/site]
|
|
1032
1083
|
filegrc validate [root] [--json]
|
|
1033
1084
|
filegrc model [--json|--write-docs|--check-docs]
|
|
1034
|
-
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]
|
|
1035
1086
|
filegrc describe <resource-type>
|
|
1036
1087
|
filegrc types [--json]
|
|
1037
1088
|
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
@@ -1054,6 +1105,8 @@ Usage:
|
|
|
1054
1105
|
filegrc review-applicability [--scaffold --type requirement|control|commitment|complementary-control] [decisions.json|-] [--preview|--yes] [--json]
|
|
1055
1106
|
filegrc review-collection <resource-type> [--scaffold | review.json|-] [--preview|--yes] [--json]
|
|
1056
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]
|
|
1109
|
+
filegrc policy-library [--json | --accept proposal-id --proposal-revision revision --yes]
|
|
1057
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]
|
|
1058
1111
|
filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
1059
1112
|
filegrc get [resource-type] <id> [--mutation]
|
|
@@ -1120,19 +1173,20 @@ Options:
|
|
|
1120
1173
|
}
|
|
1121
1174
|
if (command === "migrate") {
|
|
1122
1175
|
console.log(`Usage:
|
|
1123
|
-
filegrc migrate --to-model
|
|
1176
|
+
filegrc migrate --to-model <${SUPPORTED_MODEL_VERSIONS.join("|")}> [options]
|
|
1124
1177
|
|
|
1125
1178
|
Upgrade a workspace through an explicit, reviewable model boundary. Model v1
|
|
1126
|
-
workspaces migrate to v2 first.
|
|
1179
|
+
workspaces migrate to v2 first. Continue one version at a time through v${ACTIVE_MODEL_VERSION}. v4 separates
|
|
1127
1180
|
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1128
|
-
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
|
|
1129
1183
|
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1130
1184
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1131
1185
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
1132
1186
|
|
|
1133
1187
|
Options:
|
|
1134
|
-
--to-model <version> Required target model; migrations must run in order through
|
|
1135
|
-
--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
|
|
1136
1190
|
--preview Show the complete atomic record plan without writing
|
|
1137
1191
|
--job-title <title> Actual job title for the former Policy Owner seed person
|
|
1138
1192
|
--starts-on <date> Effective date of a new Policy Owner Appointment
|
|
@@ -1142,7 +1196,7 @@ Options:
|
|
|
1142
1196
|
--help Show this help
|
|
1143
1197
|
|
|
1144
1198
|
Start with:
|
|
1145
|
-
npx filegrc migrate --to-model
|
|
1199
|
+
npx filegrc migrate --to-model ${ACTIVE_MODEL_VERSION} --preview --json`);
|
|
1146
1200
|
return;
|
|
1147
1201
|
}
|
|
1148
1202
|
if (command === "program-readiness") {
|
|
@@ -1223,6 +1277,48 @@ Options:
|
|
|
1223
1277
|
--help Show this help`);
|
|
1224
1278
|
return;
|
|
1225
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
|
+
}
|
|
1304
|
+
if (command === "policy-library") {
|
|
1305
|
+
console.log(`Usage:
|
|
1306
|
+
filegrc policy-library [--json]
|
|
1307
|
+
filegrc policy-library --accept <proposal-id> --proposal-revision <revision> --yes [--json]
|
|
1308
|
+
|
|
1309
|
+
Review optional starter-library updates for unchanged default Policy and Control
|
|
1310
|
+
content. The review prints exact diffs. FileGRC skips customized or adopted Policy
|
|
1311
|
+
content and writes nothing until you accept one named proposal revision with --yes.
|
|
1312
|
+
|
|
1313
|
+
Options:
|
|
1314
|
+
--accept <id> Accept one proposal after reviewing its diff
|
|
1315
|
+
--proposal-revision <hash> Confirm the exact reviewed proposal revision
|
|
1316
|
+
--yes Confirm the named proposal write
|
|
1317
|
+
--json Print the versioned proposal or acceptance result
|
|
1318
|
+
--root <path> Workspace path
|
|
1319
|
+
--help Show this help`);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1226
1322
|
if (command === "evidence-map") {
|
|
1227
1323
|
console.log(`Usage:
|
|
1228
1324
|
filegrc evidence-map [options]
|
|
@@ -1251,7 +1347,7 @@ function agentOverview(model) {
|
|
|
1251
1347
|
build: "filegrc build [root]",
|
|
1252
1348
|
validate: "filegrc validate [root] --json",
|
|
1253
1349
|
model: "filegrc model --json",
|
|
1254
|
-
migrate: "filegrc migrate --to-model
|
|
1350
|
+
migrate: "filegrc migrate --to-model 5 --preview --json",
|
|
1255
1351
|
describe: "filegrc describe <resource-type>",
|
|
1256
1352
|
types: "filegrc types --json",
|
|
1257
1353
|
guide: "filegrc guide [resource-type] --json",
|
|
@@ -1270,6 +1366,7 @@ function agentOverview(model) {
|
|
|
1270
1366
|
reconcile: "filegrc reconcile --preview --json",
|
|
1271
1367
|
externalReviewerSetup: "filegrc external-reviewer-setup [--scaffold | <reviewer.json|-> --preview] --json",
|
|
1272
1368
|
policyActivation: "filegrc activate-policies [--scaffold | <activation.json|-> --preview] --json",
|
|
1369
|
+
documentActivation: "filegrc activate-documents [--scaffold | <activation.json|-> --preview] --json",
|
|
1273
1370
|
nextAuditCycle: "filegrc next-audit-cycle <prior-audit-id> --start <date> --end <date> --preview --json",
|
|
1274
1371
|
reviewApplicability: "filegrc review-applicability <decisions.json|-> --preview --json",
|
|
1275
1372
|
reviewCollection: "filegrc review-collection <resource-type> [--scaffold | <review.json|-> --preview] --json",
|
|
@@ -1422,6 +1519,7 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1422
1519
|
evidenceReady: readiness.evidenceReady,
|
|
1423
1520
|
operating: readiness.operating,
|
|
1424
1521
|
policyActivations: readiness.policyActivations,
|
|
1522
|
+
documentActivations: readiness.documentActivations,
|
|
1425
1523
|
policyLibraryProposals: readiness.policyLibraryProposals,
|
|
1426
1524
|
stages
|
|
1427
1525
|
};
|
|
@@ -1625,6 +1723,13 @@ function summarizeProgramReadiness(result) {
|
|
|
1625
1723
|
label,
|
|
1626
1724
|
gapCount
|
|
1627
1725
|
})),
|
|
1726
|
+
documentActivations: result.documentActivations.map(({ documentId, title, state, label, gapCount }) => ({
|
|
1727
|
+
documentId,
|
|
1728
|
+
title,
|
|
1729
|
+
state,
|
|
1730
|
+
label,
|
|
1731
|
+
gapCount
|
|
1732
|
+
})),
|
|
1628
1733
|
policyLibraryProposals: result.policyLibraryProposals,
|
|
1629
1734
|
unresolvedOwnership: {
|
|
1630
1735
|
count: unresolvedOwnership.length,
|
package/src/collection-review.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { modelSupports } from "../model/index.js";
|
|
2
3
|
import { collectionRevision } from "./collection-revision.js";
|
|
3
4
|
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
4
5
|
import { applyResourceBatch } from "./files.js";
|
|
@@ -26,7 +27,7 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
|
26
27
|
record.type === "collection-review"
|
|
27
28
|
&& record.resourceType === resourceType
|
|
28
29
|
&& record.status !== "retired"
|
|
29
|
-
&& (
|
|
30
|
+
&& (!modelSupports(loaded.model, "program-scope") || (record.scopeResourceIds || []).includes(program.id))
|
|
30
31
|
));
|
|
31
32
|
const review = reviewEntry?.record || null;
|
|
32
33
|
const authoritativeSourceId = review?.decision === "externally-managed"
|
|
@@ -86,7 +87,7 @@ export async function scaffoldCollectionReview(input = process.cwd(), options =
|
|
|
86
87
|
rationale: null,
|
|
87
88
|
reviewedByIds: [],
|
|
88
89
|
reviewedOn: null,
|
|
89
|
-
...(
|
|
90
|
+
...(modelSupports(loaded.model, "program-scope")
|
|
90
91
|
? { authoritativeComponentId: null }
|
|
91
92
|
: { authoritativeSystemId: null })
|
|
92
93
|
};
|
|
@@ -103,7 +104,7 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
103
104
|
const reviewedByIds = [...new Set((options.reviewedByIds || []).map(String).filter(Boolean))];
|
|
104
105
|
const reviewedOn = String(options.reviewedOn || "").trim();
|
|
105
106
|
const scopeRevision = String(options.scopeRevision || getGitSummary(loaded.root).commit || "uncommitted").trim();
|
|
106
|
-
const v4 =
|
|
107
|
+
const v4 = modelSupports(loaded.model, "program-scope");
|
|
107
108
|
const authoritativeSourceId = String(v4 ? options.authoritativeComponentId : options.authoritativeSystemId || "").trim();
|
|
108
109
|
if (!(configuration.decisions || ["complete"]).includes(decision)) {
|
|
109
110
|
throw new Error(
|
package/src/collection-scope.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { modelSupports } from "../model/index.js";
|
|
1
2
|
import { programComponents, selectedRequirementIds } from "./program.js";
|
|
2
3
|
|
|
3
4
|
export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
4
|
-
if (
|
|
5
|
+
if (!modelSupports(loaded.model, "program-scope")) {
|
|
5
6
|
return loaded.resources.filter((record) => record.type === resourceType);
|
|
6
7
|
}
|
|
7
8
|
if (resourceType === "vendor") {
|
|
@@ -37,7 +38,7 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
|
37
38
|
|
|
38
39
|
export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
39
40
|
const reviewed = scopedCollectionRecords(loaded, resourceType, program);
|
|
40
|
-
if (
|
|
41
|
+
if (!modelSupports(loaded.model, "program-scope")) {
|
|
41
42
|
return reviewed.map((record) => ({ record, value: record }));
|
|
42
43
|
}
|
|
43
44
|
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
@@ -94,7 +95,7 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export function collectionScopeRevisionFacts(loaded, resourceType, program) {
|
|
97
|
-
if (
|
|
98
|
+
if (!modelSupports(loaded.model, "program-scope")) {
|
|
98
99
|
const common = { programId: program?.id ?? null };
|
|
99
100
|
if (resourceType === "framework") {
|
|
100
101
|
return {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { applyDocumentActivationBatch, contentRevision } from "./files.js";
|
|
2
|
+
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
3
|
+
import { modelSupports } from "../model/index.js";
|
|
4
|
+
import { assessAuditDocumentActivations } from "./audit-preparation.js";
|
|
5
|
+
import { assessProgramReadiness } from "./program-readiness.js";
|
|
6
|
+
import { personWasActiveOn } from "./soc2.js";
|
|
7
|
+
import { currentCalendarDate } from "./time.js";
|
|
8
|
+
import { loadWorkspace } from "./workspace.js";
|
|
9
|
+
|
|
10
|
+
export async function scaffoldDocumentActivation(input = process.cwd(), options = {}) {
|
|
11
|
+
const loaded = await loadWorkspace(input);
|
|
12
|
+
requireDocumentLifecycle(loaded);
|
|
13
|
+
const candidates = await activationCandidates(loaded, options);
|
|
14
|
+
const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
15
|
+
const today = currentCalendarDate(loaded.workspace.timezone);
|
|
16
|
+
return {
|
|
17
|
+
documentIds: candidates.map(({ documentId }) => documentId),
|
|
18
|
+
...(options.auditId ? { auditId: options.auditId, workflowScope: "engagement" } : { workflowScope: "program" }),
|
|
19
|
+
activatedByIds: [],
|
|
20
|
+
activatedOn: today,
|
|
21
|
+
effectiveOn: today,
|
|
22
|
+
expectedRevisions: Object.fromEntries(candidates.map(({ documentId }) => [documentId, revisionById.get(documentId)])),
|
|
23
|
+
confirmed: false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function planDocumentActivation(input = process.cwd(), options = {}) {
|
|
28
|
+
const loaded = await loadWorkspace(input);
|
|
29
|
+
requireDocumentLifecycle(loaded);
|
|
30
|
+
const documentIds = [...new Set((options.documentIds || []).map(String))];
|
|
31
|
+
if (!documentIds.length) throw new Error("Document activation needs at least one approved governed Document.");
|
|
32
|
+
const activatedByIds = [...new Set((options.activatedByIds || []).map(String))];
|
|
33
|
+
if (!activatedByIds.length) throw new Error("Document activation needs the Person who performed the activation.");
|
|
34
|
+
const activatedOn = String(options.activatedOn || "").trim();
|
|
35
|
+
const effectiveOn = String(options.effectiveOn || "").trim();
|
|
36
|
+
if (!isCalendarDate(activatedOn)) throw new Error("Document activation needs the actual activation date in YYYY-MM-DD format.");
|
|
37
|
+
if (!isCalendarDate(effectiveOn)) throw new Error("Document activation needs a real effective date in YYYY-MM-DD format.");
|
|
38
|
+
const today = currentCalendarDate(loaded.workspace.timezone);
|
|
39
|
+
if (activatedOn !== today) {
|
|
40
|
+
throw new Error(`The activation date records today's lifecycle event and must be ${today}. Keep a future date in proposedEffectiveOn until activation occurs.`);
|
|
41
|
+
}
|
|
42
|
+
if (effectiveOn < activatedOn) {
|
|
43
|
+
throw new Error("The effective date cannot be before the separate activation date; do not backdate adoption.");
|
|
44
|
+
}
|
|
45
|
+
const expectedRevisions = options.expectedRevisions || {};
|
|
46
|
+
if (Array.isArray(expectedRevisions) || !expectedRevisions || typeof expectedRevisions !== "object") {
|
|
47
|
+
throw new Error("Document activation expected revisions must be keyed by Document ID.");
|
|
48
|
+
}
|
|
49
|
+
const workflowScope = options.auditId ? "engagement" : "program";
|
|
50
|
+
const candidates = await activationCandidates(loaded, options);
|
|
51
|
+
const assessmentById = new Map(candidates.map((item) => [item.documentId, item]));
|
|
52
|
+
const entryById = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
53
|
+
const invalidActivatorIds = activatedByIds.filter((id) => {
|
|
54
|
+
const person = entryById.get(id)?.record;
|
|
55
|
+
return !personWasActiveOn(person, activatedOn);
|
|
56
|
+
});
|
|
57
|
+
if (invalidActivatorIds.length) {
|
|
58
|
+
throw new Error(`Document activation needs active People as activators: ${invalidActivatorIds.join(", ")}.`);
|
|
59
|
+
}
|
|
60
|
+
const update = documentIds.map((documentId) => {
|
|
61
|
+
const entry = entryById.get(documentId);
|
|
62
|
+
if (!entry || entry.record.type !== "document") throw new Error(`Document "${documentId}" was not found.`);
|
|
63
|
+
if (entry.record.status !== "approved") {
|
|
64
|
+
throw new Error(`Document "${documentId}" must be independently approved before its separate ${workflowScope === "engagement" ? "Step 5" : "Step 3"} activation.`);
|
|
65
|
+
}
|
|
66
|
+
if (entry.record.workflowScope !== workflowScope) {
|
|
67
|
+
throw new Error(`Document "${documentId}" belongs to the ${entry.record.workflowScope} workflow, not ${workflowScope}.`);
|
|
68
|
+
}
|
|
69
|
+
const assessment = assessmentById.get(documentId);
|
|
70
|
+
if (assessment?.state !== "ready-to-activate") {
|
|
71
|
+
const missing = assessment?.missingImplementationControlIds || [];
|
|
72
|
+
throw new Error(
|
|
73
|
+
missing.length
|
|
74
|
+
? `Document "${documentId}" cannot be activated until linked Controls are implemented: ${missing.join(", ")}.`
|
|
75
|
+
: `Document "${documentId}" is not ready for ${workflowScope === "engagement" ? "Step 5" : "Step 3"} activation.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (!/^[a-f0-9]{64}$/.test(expectedRevisions[documentId] || "")) {
|
|
79
|
+
throw new Error(`Document activation needs the current record revision for "${documentId}". Regenerate the activation review and try again.`);
|
|
80
|
+
}
|
|
81
|
+
const record = {
|
|
82
|
+
...entry.record,
|
|
83
|
+
status: "active",
|
|
84
|
+
activationBasis: "recorded",
|
|
85
|
+
activatedByIds,
|
|
86
|
+
activatedOn,
|
|
87
|
+
effectiveOn,
|
|
88
|
+
activatedContentRevisions: structuredClone(entry.record.approvedContentRevisions)
|
|
89
|
+
};
|
|
90
|
+
delete record.proposedEffectiveOn;
|
|
91
|
+
return record;
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
operation: "document-activation",
|
|
95
|
+
workflowScope,
|
|
96
|
+
...(options.auditId ? { auditId: options.auditId } : {}),
|
|
97
|
+
documentIds,
|
|
98
|
+
activatedByIds,
|
|
99
|
+
activatedOn,
|
|
100
|
+
effectiveOn,
|
|
101
|
+
changes: {
|
|
102
|
+
update,
|
|
103
|
+
expectedRevisions: Object.fromEntries(documentIds.map((documentId) => [documentId, expectedRevisions[documentId]])),
|
|
104
|
+
validateWholeWorkspace: true
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function activationCandidates(loaded, options) {
|
|
110
|
+
if (options.auditId) {
|
|
111
|
+
return (await assessAuditDocumentActivations(loaded, {
|
|
112
|
+
auditId: options.auditId,
|
|
113
|
+
asOf: currentCalendarDate(loaded.workspace.timezone)
|
|
114
|
+
})).filter(({ state }) => state === "ready-to-activate");
|
|
115
|
+
}
|
|
116
|
+
const readiness = await assessProgramReadiness(loaded, { programId: options.programId });
|
|
117
|
+
return readiness.documentActivations.filter(({ state }) => state === "ready-to-activate");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function activateDocuments(input = process.cwd(), options = {}) {
|
|
121
|
+
if (options.confirmed !== true) throw new Error("Review the governed Document activation and confirm the write.");
|
|
122
|
+
return serializeWorkspaceMutation(input, async (root) => {
|
|
123
|
+
const plan = await planDocumentActivation(root, options);
|
|
124
|
+
const result = await applyDocumentActivationBatch(root, plan.changes);
|
|
125
|
+
return { ...plan, result };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requireDocumentLifecycle(loaded) {
|
|
130
|
+
if (!modelSupports(loaded.model, "governed-document-activation")) {
|
|
131
|
+
throw new Error("Separate governed Document approval and activation requires a model v5 workspace.");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isCalendarDate(value) {
|
|
136
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
137
|
+
if (!match) return false;
|
|
138
|
+
const [, year, month, day] = match.map(Number);
|
|
139
|
+
const date = new Date(0);
|
|
140
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
141
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
142
|
+
return date.getUTCFullYear() === year
|
|
143
|
+
&& date.getUTCMonth() === month - 1
|
|
144
|
+
&& date.getUTCDate() === day;
|
|
145
|
+
}
|