filegrc 0.9.1 → 0.10.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/model/index.js +4 -4
- package/model/v7.json +10359 -0
- package/package.json +1 -1
- package/src/applicability-scope.js +211 -0
- package/src/audit-preparation.js +156 -20
- package/src/batch-review.js +40 -24
- package/src/cli.js +20 -7
- package/src/collection-review.js +16 -3
- package/src/collection-revision.js +29 -4
- package/src/collection-scope.js +110 -11
- package/src/document-activation.js +13 -1
- package/src/model-migration.js +224 -34
- package/src/obligations.js +128 -31
- package/src/policy-activation.js +5 -0
- package/src/program-lifecycle.js +1 -1
- package/src/program-path.js +1 -4
- package/src/program-readiness.js +20 -3
- package/src/reconciliation.js +15 -3
- package/src/server.js +2 -1
- package/src/setup.js +1 -1
- package/src/state.js +2 -1
- package/src/validate.js +25 -7
- package/src/web.js +121 -43
- package/src/workflow.js +7 -6
package/src/cli.js
CHANGED
|
@@ -223,6 +223,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
223
223
|
: `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
|
|
224
224
|
} else {
|
|
225
225
|
console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
|
|
226
|
+
if (result.migrationReportPath) console.log(`Migration report: ${result.migrationReportPath}`);
|
|
226
227
|
}
|
|
227
228
|
return result;
|
|
228
229
|
}
|
|
@@ -261,7 +262,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
261
262
|
const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
|
|
262
263
|
const programId = flags.program || audit?.programId;
|
|
263
264
|
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
|
|
264
|
-
const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
|
|
265
|
+
const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
|
|
266
|
+
auditId,
|
|
267
|
+
asOf: flags["as-of"]
|
|
268
|
+
}) : null;
|
|
265
269
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
266
270
|
const output = selectProgramPathOutput(result, flags);
|
|
267
271
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
@@ -481,7 +485,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
481
485
|
}
|
|
482
486
|
if (command === "audit-readiness") {
|
|
483
487
|
const loaded = await loadWorkspace(root);
|
|
484
|
-
const result = await assessAuditPreparation(loaded, {
|
|
488
|
+
const result = await assessAuditPreparation(loaded, {
|
|
489
|
+
auditId: positionals[0] || flags.audit,
|
|
490
|
+
asOf: flags["as-of"]
|
|
491
|
+
});
|
|
485
492
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
486
493
|
else {
|
|
487
494
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} management items complete`);
|
|
@@ -1115,7 +1122,7 @@ Usage:
|
|
|
1115
1122
|
filegrc build [root] [--output .filegrc/site]
|
|
1116
1123
|
filegrc validate [root] [--json]
|
|
1117
1124
|
filegrc model [--json|--write-docs|--check-docs]
|
|
1118
|
-
filegrc migrate --to-model
|
|
1125
|
+
filegrc migrate --to-model <${SUPPORTED_MODEL_VERSIONS.join("|")}> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
1119
1126
|
filegrc describe <resource-type>
|
|
1120
1127
|
filegrc types [--json]
|
|
1121
1128
|
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
@@ -1129,7 +1136,7 @@ Usage:
|
|
|
1129
1136
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
1130
1137
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
1131
1138
|
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
1132
|
-
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
1139
|
+
filegrc audit-readiness [audit-id] [--as-of YYYY-MM-DD] [--require-ready] [--json]
|
|
1133
1140
|
filegrc prepare-audit <audit-id> [--json]
|
|
1134
1141
|
filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes] [--risk-level normal|high] [--json]
|
|
1135
1142
|
filegrc external-reviewer-setup --scaffold
|
|
@@ -1214,7 +1221,8 @@ workspaces migrate to v2 first. Continue one version at a time through v${ACTIVE
|
|
|
1214
1221
|
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1215
1222
|
specific Assets, Vendors, normalized information, and Evidence Artifacts. Model v5 separates
|
|
1216
1223
|
Document approval from activation and records program-versus-engagement scope. Model v6 gives
|
|
1217
|
-
Training the same approval and activation split and moves its schedule into Obligations.
|
|
1224
|
+
Training the same approval and activation split and moves its schedule into Obligations. Model v7
|
|
1225
|
+
keeps issued historical Documents neutral and requires a current activation for legacy Training. v3 migration
|
|
1218
1226
|
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1219
1227
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1220
1228
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
@@ -1231,7 +1239,8 @@ Options:
|
|
|
1231
1239
|
--help Show this help
|
|
1232
1240
|
|
|
1233
1241
|
Start with:
|
|
1234
|
-
npx filegrc
|
|
1242
|
+
npx filegrc guide --json
|
|
1243
|
+
# Use the next model version shown by the guide.`);
|
|
1235
1244
|
return;
|
|
1236
1245
|
}
|
|
1237
1246
|
if (command === "program-readiness") {
|
|
@@ -1398,6 +1407,10 @@ Options:
|
|
|
1398
1407
|
}
|
|
1399
1408
|
|
|
1400
1409
|
function agentOverview(model) {
|
|
1410
|
+
const currentModelIndex = SUPPORTED_MODEL_VERSIONS.indexOf(String(model.modelVersion));
|
|
1411
|
+
const nextModelVersion = currentModelIndex >= 0 && currentModelIndex < SUPPORTED_MODEL_VERSIONS.length - 1
|
|
1412
|
+
? SUPPORTED_MODEL_VERSIONS[currentModelIndex + 1]
|
|
1413
|
+
: ACTIVE_MODEL_VERSION;
|
|
1401
1414
|
const commands = {
|
|
1402
1415
|
help: "filegrc help",
|
|
1403
1416
|
version: "filegrc version",
|
|
@@ -1406,7 +1419,7 @@ function agentOverview(model) {
|
|
|
1406
1419
|
build: "filegrc build [root]",
|
|
1407
1420
|
validate: "filegrc validate [root] --json",
|
|
1408
1421
|
model: "filegrc model --json",
|
|
1409
|
-
migrate:
|
|
1422
|
+
migrate: `filegrc migrate --to-model ${nextModelVersion} --preview --json`,
|
|
1410
1423
|
describe: "filegrc describe <resource-type>",
|
|
1411
1424
|
types: "filegrc types --json",
|
|
1412
1425
|
guide: "filegrc guide [resource-type] --json",
|
package/src/collection-review.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { modelSupports } from "../model/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
collectionRevision,
|
|
5
|
+
collectionRevisionMatches
|
|
6
|
+
} from "./collection-revision.js";
|
|
4
7
|
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
5
8
|
import { applyResourceBatch } from "./files.js";
|
|
6
9
|
import { getGitSummary } from "./git.js";
|
|
@@ -41,15 +44,25 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
|
41
44
|
const allowsEmptyCollection = allowedDecisions.some((decision) => (
|
|
42
45
|
decision === "zero-population" || decision === "externally-managed"
|
|
43
46
|
));
|
|
47
|
+
const revisionMatches = collectionRevisionMatches(
|
|
48
|
+
loaded,
|
|
49
|
+
resourceType,
|
|
50
|
+
review?.collectionRevision,
|
|
51
|
+
{
|
|
52
|
+
programId: program.id,
|
|
53
|
+
authoritativeSourceId,
|
|
54
|
+
currentRevision
|
|
55
|
+
}
|
|
56
|
+
);
|
|
44
57
|
const complete = Boolean(
|
|
45
58
|
review?.status === "active"
|
|
46
59
|
&& allowedDecisions.includes(review.decision)
|
|
47
|
-
&&
|
|
60
|
+
&& revisionMatches
|
|
48
61
|
);
|
|
49
62
|
const stale = Boolean(
|
|
50
63
|
review?.status === "active"
|
|
51
64
|
&& review.collectionRevision
|
|
52
|
-
&&
|
|
65
|
+
&& !revisionMatches
|
|
53
66
|
);
|
|
54
67
|
return {
|
|
55
68
|
resourceType,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
|
+
authoritativeSourceRevisionValue,
|
|
4
5
|
collectionRevisionInputs,
|
|
5
6
|
collectionScopeRevisionFacts
|
|
6
7
|
} from "./collection-scope.js";
|
|
@@ -9,22 +10,46 @@ import { resolveProgram } from "./program.js";
|
|
|
9
10
|
import { markdownEntries } from "./resource-markdown.js";
|
|
10
11
|
|
|
11
12
|
export function collectionRevision(loaded, resourceType, options = {}) {
|
|
13
|
+
return calculateCollectionRevision(loaded, resourceType, options, false);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function legacyCollectionRevision(loaded, resourceType, options = {}) {
|
|
17
|
+
return calculateCollectionRevision(loaded, resourceType, options, true);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function collectionRevisionMatches(loaded, resourceType, storedRevision, options = {}) {
|
|
21
|
+
if (!storedRevision) return false;
|
|
22
|
+
const currentRevision = options.currentRevision
|
|
23
|
+
|| collectionRevision(loaded, resourceType, options);
|
|
24
|
+
// Version 0.9.2 narrowed this hash basis. Keep unchanged 0.9.1 reviews valid
|
|
25
|
+
// until management records a new review on the current basis.
|
|
26
|
+
return storedRevision === currentRevision
|
|
27
|
+
|| storedRevision === legacyCollectionRevision(loaded, resourceType, options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function calculateCollectionRevision(loaded, resourceType, options, legacy) {
|
|
12
31
|
const program = Object.hasOwn(options, "program")
|
|
13
32
|
? options.program
|
|
14
33
|
: resolveProgram(loaded, options.programId);
|
|
15
|
-
const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program)
|
|
34
|
+
const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program, { legacy })
|
|
16
35
|
.map((input) => [input.record.id, input]));
|
|
17
36
|
const authoritativeSource = loaded.resources.find(({ id }) => id === options.authoritativeSourceId);
|
|
18
37
|
if (authoritativeSource) {
|
|
19
|
-
inputs.set(authoritativeSource.id, {
|
|
38
|
+
inputs.set(authoritativeSource.id, {
|
|
39
|
+
record: authoritativeSource,
|
|
40
|
+
value: legacy
|
|
41
|
+
? authoritativeSource
|
|
42
|
+
: authoritativeSourceRevisionValue(authoritativeSource),
|
|
43
|
+
includeContent: true
|
|
44
|
+
});
|
|
20
45
|
}
|
|
21
46
|
const records = [...inputs.values()]
|
|
22
|
-
.map(({ record, value }) => ({
|
|
47
|
+
.map(({ record, value, includeContent }) => ({
|
|
23
48
|
id: record.id,
|
|
24
49
|
revision: createHash("sha256")
|
|
25
50
|
.update(JSON.stringify(canonicalRecordValue(loaded.model, record.type, value)))
|
|
26
51
|
.digest("hex"),
|
|
27
|
-
contentRevisions: markdownEntries(loaded.model, record).flatMap(({ path }) => {
|
|
52
|
+
contentRevisions: (legacy || includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
|
|
28
53
|
try {
|
|
29
54
|
const content = readFileSync(resolveDataPath(loaded.root, path), "utf8");
|
|
30
55
|
return [{ path, revision: createHash("sha256").update(content).digest("hex") }];
|
package/src/collection-scope.js
CHANGED
|
@@ -5,8 +5,27 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
|
5
5
|
if (!modelSupports(loaded.model, "program-scope")) {
|
|
6
6
|
return loaded.resources.filter((record) => record.type === resourceType);
|
|
7
7
|
}
|
|
8
|
+
if (resourceType === "person") {
|
|
9
|
+
return scopedProgramPeople(loaded, program);
|
|
10
|
+
}
|
|
8
11
|
if (resourceType === "vendor") {
|
|
9
|
-
|
|
12
|
+
const scopedVendorIds = new Set(programComponents(loaded, program).map(({ vendorId }) => vendorId).filter(Boolean));
|
|
13
|
+
const auditVendorIds = new Set(loaded.resources
|
|
14
|
+
.filter((record) => record.type === "audit" && record.auditorVendorId)
|
|
15
|
+
.map(({ auditorVendorId }) => auditorVendorId));
|
|
16
|
+
const auditOnlyVendorIds = Number(loaded.model.modelVersion) >= 7
|
|
17
|
+
? auditVendorIds
|
|
18
|
+
: new Set(loaded.resources
|
|
19
|
+
.filter((record) => (
|
|
20
|
+
record.type === "vendor"
|
|
21
|
+
&& auditVendorIds.has(record.id)
|
|
22
|
+
&& /(?:audit|accounting|cpa)/i.test(record.category || "")
|
|
23
|
+
))
|
|
24
|
+
.map(({ id }) => id));
|
|
25
|
+
return loaded.resources.filter((record) => (
|
|
26
|
+
record.type === "vendor"
|
|
27
|
+
&& (!auditOnlyVendorIds.has(record.id) || scopedVendorIds.has(record.id))
|
|
28
|
+
));
|
|
10
29
|
}
|
|
11
30
|
const scopedProgram = program || {};
|
|
12
31
|
const components = programComponents(loaded, scopedProgram);
|
|
@@ -36,10 +55,47 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
|
36
55
|
));
|
|
37
56
|
}
|
|
38
57
|
|
|
39
|
-
|
|
58
|
+
function scopedProgramPeople(loaded, program = {}) {
|
|
59
|
+
const personIds = new Set(loaded.resources.filter(({ type }) => type === "person").map(({ id }) => id));
|
|
60
|
+
const systemIds = new Set(program.systemIds || []);
|
|
61
|
+
const controlIds = new Set(program.controlIds || []);
|
|
62
|
+
const policyIds = new Set(loaded.resources
|
|
63
|
+
.filter(({ type, status, programRole }) => type === "policy" && status !== "retired" && programRole !== "reference")
|
|
64
|
+
.map(({ id }) => id));
|
|
65
|
+
const components = programComponents(loaded, program);
|
|
66
|
+
const componentIds = new Set(components.map(({ id }) => id));
|
|
67
|
+
const vendorIds = new Set([
|
|
68
|
+
...components.map(({ vendorId }) => vendorId).filter(Boolean),
|
|
69
|
+
...scopedCollectionRecords(loaded, "vendor", program).map(({ id }) => id)
|
|
70
|
+
]);
|
|
71
|
+
const sources = loaded.resources.filter((record) => (
|
|
72
|
+
["workspace", "program", "appointment", "team"].includes(record.type)
|
|
73
|
+
|| record.type === "system" && systemIds.has(record.id)
|
|
74
|
+
|| record.type === "component" && componentIds.has(record.id)
|
|
75
|
+
|| record.type === "vendor" && vendorIds.has(record.id)
|
|
76
|
+
|| record.type === "control" && controlIds.has(record.id)
|
|
77
|
+
|| record.type === "policy" && policyIds.has(record.id)
|
|
78
|
+
|| record.type === "document" && record.workflowScope !== "engagement"
|
|
79
|
+
|| record.type === "obligation" && (
|
|
80
|
+
(record.controlIds || []).some((id) => controlIds.has(id))
|
|
81
|
+
|| (record.policyIds || []).some((id) => policyIds.has(id))
|
|
82
|
+
)
|
|
83
|
+
));
|
|
84
|
+
const selected = new Set();
|
|
85
|
+
const visit = (value) => {
|
|
86
|
+
if (typeof value === "string" && personIds.has(value)) selected.add(value);
|
|
87
|
+
else if (Array.isArray(value)) value.forEach(visit);
|
|
88
|
+
else if (value && typeof value === "object") Object.values(value).forEach(visit);
|
|
89
|
+
};
|
|
90
|
+
sources.forEach(visit);
|
|
91
|
+
return loaded.resources.filter((record) => record.type === "person" && selected.has(record.id));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function collectionRevisionInputs(loaded, resourceType, program, options = {}) {
|
|
95
|
+
const legacy = options.legacy === true;
|
|
40
96
|
const reviewed = scopedCollectionRecords(loaded, resourceType, program);
|
|
41
97
|
if (!modelSupports(loaded.model, "program-scope")) {
|
|
42
|
-
return reviewed.map((record) => ({ record, value: record }));
|
|
98
|
+
return reviewed.map((record) => ({ record, value: record, includeContent: true }));
|
|
43
99
|
}
|
|
44
100
|
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
45
101
|
const records = new Map(reviewed.map((record) => [record.id, record]));
|
|
@@ -90,7 +146,10 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
|
90
146
|
record,
|
|
91
147
|
value: reviewedIds.has(record.id)
|
|
92
148
|
? record
|
|
93
|
-
: dependencyRevisionValue(resourceType, record)
|
|
149
|
+
: dependencyRevisionValue(resourceType, record, legacy),
|
|
150
|
+
includeContent: legacy
|
|
151
|
+
|| reviewedIds.has(record.id)
|
|
152
|
+
|| dependencyContentAffectsRevision(resourceType, record.type)
|
|
94
153
|
}));
|
|
95
154
|
}
|
|
96
155
|
|
|
@@ -138,9 +197,20 @@ export function collectionScopeRevisionFacts(loaded, resourceType, program) {
|
|
|
138
197
|
return common;
|
|
139
198
|
}
|
|
140
199
|
|
|
200
|
+
export function authoritativeSourceRevisionValue(record) {
|
|
201
|
+
return Object.fromEntries(Object.entries(record).filter(([field]) => (
|
|
202
|
+
!authoritativeSourceBookkeepingFields.has(field)
|
|
203
|
+
)));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const authoritativeSourceBookkeepingFields = new Set([
|
|
207
|
+
"tags",
|
|
208
|
+
"statusTransition"
|
|
209
|
+
]);
|
|
210
|
+
|
|
141
211
|
const dependencyFields = {
|
|
142
212
|
framework: {
|
|
143
|
-
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
213
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
144
214
|
requirement: ["id", "type", "title", "frameworkId", "reference", "description", "parentRequirementId"]
|
|
145
215
|
},
|
|
146
216
|
vendor: {
|
|
@@ -154,30 +224,59 @@ const dependencyFields = {
|
|
|
154
224
|
},
|
|
155
225
|
component: {
|
|
156
226
|
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
157
|
-
control: ["id", "type", "
|
|
158
|
-
vendor: ["id", "type", "status", "category", "criticality", "description", "
|
|
227
|
+
control: ["id", "type", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
228
|
+
vendor: ["id", "type", "status", "category", "criticality", "description", "startDate", "endDate"],
|
|
159
229
|
classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
|
|
160
230
|
"information-type": ["id", "type", "status", "classificationId", "description"]
|
|
161
231
|
},
|
|
162
232
|
"complementary-control": {
|
|
163
|
-
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
164
|
-
control: ["id", "type", "status", "statement", "activity", "
|
|
233
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
234
|
+
control: ["id", "type", "status", "statement", "activity", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
165
235
|
vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"],
|
|
166
236
|
requirement: ["id", "type", "title", "frameworkId", "reference", "description", "parentRequirementId"],
|
|
167
237
|
commitment: ["id", "type", "status", "commitmentKind", "statement", "systemIds", "requirementIds", "controlIds", "customerFacing", "effectiveOn"],
|
|
168
238
|
document: ["id", "type", "status", "documentKind", "version", "effectiveOn", "approvedOn", "approvedContentRevisions", "systemIds", "controlIds", "componentIds", "classificationId"],
|
|
239
|
+
component: ["id", "type", "status", "componentKind", "description", "criticality", "vendorId", "systemUses", "informationUses", "internetExposed", "classificationId", "continuityObjectives"]
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const legacyDependencyFieldOverrides = {
|
|
244
|
+
framework: {
|
|
245
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"]
|
|
246
|
+
},
|
|
247
|
+
component: {
|
|
248
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
249
|
+
vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"]
|
|
250
|
+
},
|
|
251
|
+
"complementary-control": {
|
|
252
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
253
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
169
254
|
component: ["id", "type", "status", "componentKind", "description", "vendorId", "systemUses", "informationUses", "internetExposed"]
|
|
170
255
|
}
|
|
171
256
|
};
|
|
172
257
|
|
|
173
|
-
|
|
174
|
-
|
|
258
|
+
const dependencyContentTypes = {
|
|
259
|
+
framework: new Set(["system"]),
|
|
260
|
+
vendor: new Set(["document"]),
|
|
261
|
+
component: new Set(["system"]),
|
|
262
|
+
"complementary-control": new Set(["system", "control", "document", "component"])
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
function dependencyRevisionValue(resourceType, record, legacy) {
|
|
266
|
+
const fields = legacy
|
|
267
|
+
? legacyDependencyFieldOverrides[resourceType]?.[record.type]
|
|
268
|
+
|| dependencyFields[resourceType]?.[record.type]
|
|
269
|
+
: dependencyFields[resourceType]?.[record.type];
|
|
175
270
|
if (!fields) return { id: record.id, type: record.type };
|
|
176
271
|
return Object.fromEntries(fields
|
|
177
272
|
.filter((field) => record[field] !== undefined)
|
|
178
273
|
.map((field) => [field, record[field]]));
|
|
179
274
|
}
|
|
180
275
|
|
|
276
|
+
function dependencyContentAffectsRevision(resourceType, dependencyType) {
|
|
277
|
+
return dependencyContentTypes[resourceType]?.has(dependencyType) || false;
|
|
278
|
+
}
|
|
279
|
+
|
|
181
280
|
function sorted(values) {
|
|
182
281
|
return [...(values || [])].sort();
|
|
183
282
|
}
|
|
@@ -14,6 +14,13 @@ export async function scaffoldDocumentActivation(input = process.cwd(), options
|
|
|
14
14
|
const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
15
15
|
const today = currentCalendarDate(loaded.workspace.timezone);
|
|
16
16
|
return {
|
|
17
|
+
available: candidates.length > 0,
|
|
18
|
+
message: candidates.length
|
|
19
|
+
? `${candidates.length} approved ${candidates.length === 1 ? "Document is" : "Documents are"} ready to activate.`
|
|
20
|
+
: `No ${options.auditId ? "engagement Document" : "program Document"} is ready to activate. Review the current readiness actions first.`,
|
|
21
|
+
nextCommand: candidates.length ? null : options.auditId
|
|
22
|
+
? `npx filegrc audit-readiness ${options.auditId} --json`
|
|
23
|
+
: "npx filegrc program-path --next --json",
|
|
17
24
|
documentIds: candidates.filter(({ resourceType }) => resourceType === "document").map(({ resourceId }) => resourceId),
|
|
18
25
|
...(options.auditId ? { auditId: options.auditId, workflowScope: "engagement" } : { workflowScope: "program" }),
|
|
19
26
|
activatedByIds: [],
|
|
@@ -141,12 +148,17 @@ export async function scaffoldGovernedContentActivation(input = process.cwd(), o
|
|
|
141
148
|
const loaded = await loadWorkspace(input);
|
|
142
149
|
requireDocumentLifecycle(loaded);
|
|
143
150
|
if (!modelSupports(loaded.model, "governed-training-activation")) {
|
|
144
|
-
throw new Error("Unified governed-content activation requires a model v6 workspace.");
|
|
151
|
+
throw new Error("Unified governed-content activation requires a model v6 or later workspace.");
|
|
145
152
|
}
|
|
146
153
|
const candidates = await activationCandidates(loaded, { ...options, auditId: undefined });
|
|
147
154
|
const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
148
155
|
const today = currentCalendarDate(loaded.workspace.timezone);
|
|
149
156
|
return {
|
|
157
|
+
available: candidates.length > 0,
|
|
158
|
+
message: candidates.length
|
|
159
|
+
? `${candidates.length} approved governed-content ${candidates.length === 1 ? "record is" : "records are"} ready to activate.`
|
|
160
|
+
: "No program Document or Training record is ready to activate. Review the current readiness actions first.",
|
|
161
|
+
nextCommand: candidates.length ? null : "npx filegrc program-path --next --json",
|
|
150
162
|
resourceIds: candidates.map(({ resourceId }) => resourceId),
|
|
151
163
|
documentIds: candidates.filter(({ resourceType }) => resourceType === "document").map(({ resourceId }) => resourceId),
|
|
152
164
|
trainingIds: candidates.filter(({ resourceType }) => resourceType === "training").map(({ resourceId }) => resourceId),
|