filegrc 0.9.2 → 0.11.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 +8 -5
- package/model/v7.json +10359 -0
- package/model/v8.json +10947 -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 +85 -7
- package/src/collection-review.js +16 -3
- package/src/collection-revision.js +23 -3
- package/src/collection-scope.js +94 -7
- package/src/document-activation.js +13 -1
- package/src/git.js +4 -4
- package/src/index.js +3 -0
- package/src/model-migration.js +381 -35
- package/src/obligations.js +142 -39
- package/src/policy-activation.js +5 -0
- package/src/policy-library/data-retention-schedule-v2.md +25 -0
- package/src/policy-library.js +52 -24
- package/src/program-amendment.js +222 -0
- package/src/program-lifecycle.js +1 -1
- package/src/program-path.js +13 -8
- package/src/program-readiness.js +43 -13
- package/src/reconciliation.js +15 -3
- package/src/requirement-mapping.js +61 -0
- package/src/retention.js +261 -0
- package/src/server.js +76 -2
- package/src/setup.js +1 -1
- package/src/source-coverage.js +21 -7
- package/src/state.js +171 -2
- package/src/validate.js +106 -11
- package/src/web.js +441 -70
- package/src/workflow.js +33 -13
package/src/cli.js
CHANGED
|
@@ -53,7 +53,9 @@ import { activatePolicies, planPolicyActivation, scaffoldPolicyActivation } from
|
|
|
53
53
|
import { applyPolicyLibraryUpgrade, assessPolicyLibraryUpgrades } from "./policy-library.js";
|
|
54
54
|
import { buildAgentProgramPath } from "./program-path.js";
|
|
55
55
|
import { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
56
|
+
import { planProgramAmendment } from "./program-amendment.js";
|
|
56
57
|
import { resolveProgram } from "./program.js";
|
|
58
|
+
import { resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
57
59
|
import { applyReconciliation, planReconciliation } from "./reconciliation.js";
|
|
58
60
|
import { markdownEntries } from "./resource-markdown.js";
|
|
59
61
|
import { effectiveResourceStatus } from "./resource-status.js";
|
|
@@ -223,6 +225,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
223
225
|
: `Needs review: ${result.missing.length} missing values, ${result.conflicts.length} conflicts, ${result.manualActions.length} manual actions.`);
|
|
224
226
|
} else {
|
|
225
227
|
console.log(`Migrated workspace from model v${result.sourceModelVersion} to v${result.targetModelVersion}.`);
|
|
228
|
+
if (result.migrationReportPath) console.log(`Migration report: ${result.migrationReportPath}`);
|
|
226
229
|
}
|
|
227
230
|
return result;
|
|
228
231
|
}
|
|
@@ -261,7 +264,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
261
264
|
const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
|
|
262
265
|
const programId = flags.program || audit?.programId;
|
|
263
266
|
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
|
|
264
|
-
const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
|
|
267
|
+
const auditReadiness = auditId ? await assessAuditPreparation(loaded, {
|
|
268
|
+
auditId,
|
|
269
|
+
asOf: flags["as-of"]
|
|
270
|
+
}) : null;
|
|
265
271
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
266
272
|
const output = selectProgramPathOutput(result, flags);
|
|
267
273
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
@@ -458,6 +464,46 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
458
464
|
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
459
465
|
return output;
|
|
460
466
|
}
|
|
467
|
+
if (command === "program-amendment") {
|
|
468
|
+
const sourceResourceId = flags.source || positionals[0];
|
|
469
|
+
if (!sourceResourceId) throw new Error("Pass a source resource ID or --source resource-id.");
|
|
470
|
+
const result = await planProgramAmendment(root, { sourceResourceId });
|
|
471
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
472
|
+
else {
|
|
473
|
+
console.log(`Program amendment review for ${result.source.title}`);
|
|
474
|
+
for (const [type, ids] of Object.entries(result.byResourceType)) console.log(`${type}\t${ids.join(",")}`);
|
|
475
|
+
for (const work of result.reviewWork) {
|
|
476
|
+
console.log(`REVIEW\t${work.resourceType}\t${work.resourceIds?.length ? work.resourceIds.join(",") + "\t" : ""}${work.message}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
if (command === "review-bindings") {
|
|
482
|
+
const resourceId = positionals[0];
|
|
483
|
+
if (!resourceId) throw new Error("Pass a Retention Schedule Item or Requirement Mapping ID.");
|
|
484
|
+
const loaded = await loadWorkspace(root);
|
|
485
|
+
const record = loaded.resources.find(({ id }) => id === resourceId);
|
|
486
|
+
if (!record) throw new Error(`Resource "${resourceId}" was not found.`);
|
|
487
|
+
const dependencyIds = record.type === "retention-schedule-item"
|
|
488
|
+
? retentionReviewResourceIds(record, loaded)
|
|
489
|
+
: record.type === "requirement-mapping"
|
|
490
|
+
? [...new Set([...(record.sourceResourceIds || []), ...(record.targetResourceIds || [])])]
|
|
491
|
+
: null;
|
|
492
|
+
if (!dependencyIds) throw new Error("Review bindings are available for Retention Schedule Items and Requirement Mappings.");
|
|
493
|
+
const revisions = await resourceReviewRevisions(loaded, dependencyIds);
|
|
494
|
+
const result = {
|
|
495
|
+
resource: { type: record.type, id: record.id, title: record.title },
|
|
496
|
+
dependencyIds,
|
|
497
|
+
reviewedSourceRevisions: Object.fromEntries(revisions),
|
|
498
|
+
missingResourceIds: dependencyIds.filter((id) => !revisions.has(id))
|
|
499
|
+
};
|
|
500
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
501
|
+
else {
|
|
502
|
+
console.log(`Review bindings for ${record.title}`);
|
|
503
|
+
for (const id of dependencyIds) console.log(`${revisions.has(id) ? "READY" : "MISSING"}\t${id}\t${revisions.get(id) || ""}`);
|
|
504
|
+
}
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
461
507
|
if (command === "evidence-map") {
|
|
462
508
|
const loaded = await loadWorkspace(root);
|
|
463
509
|
const result = await assessEvidenceMap(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
@@ -481,7 +527,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
481
527
|
}
|
|
482
528
|
if (command === "audit-readiness") {
|
|
483
529
|
const loaded = await loadWorkspace(root);
|
|
484
|
-
const result = await assessAuditPreparation(loaded, {
|
|
530
|
+
const result = await assessAuditPreparation(loaded, {
|
|
531
|
+
auditId: positionals[0] || flags.audit,
|
|
532
|
+
asOf: flags["as-of"]
|
|
533
|
+
});
|
|
485
534
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
486
535
|
else {
|
|
487
536
|
console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} management items complete`);
|
|
@@ -1115,7 +1164,7 @@ Usage:
|
|
|
1115
1164
|
filegrc build [root] [--output .filegrc/site]
|
|
1116
1165
|
filegrc validate [root] [--json]
|
|
1117
1166
|
filegrc model [--json|--write-docs|--check-docs]
|
|
1118
|
-
filegrc migrate --to-model
|
|
1167
|
+
filegrc migrate --to-model <${SUPPORTED_MODEL_VERSIONS.join("|")}> [--preview] [--decisions path] [--job-title text] [--starts-on YYYY-MM-DD] [--yes] [--json]
|
|
1119
1168
|
filegrc describe <resource-type>
|
|
1120
1169
|
filegrc types [--json]
|
|
1121
1170
|
filegrc guide [resource-type] [--id resource-id] [--program program-id] [--json]
|
|
@@ -1128,8 +1177,10 @@ Usage:
|
|
|
1128
1177
|
filegrc search <query> [--type resource-type] [--json]
|
|
1129
1178
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
1130
1179
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
1180
|
+
filegrc program-amendment <source-resource-id> [--json]
|
|
1181
|
+
filegrc review-bindings <retention-or-mapping-id> [--json]
|
|
1131
1182
|
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
1132
|
-
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
1183
|
+
filegrc audit-readiness [audit-id] [--as-of YYYY-MM-DD] [--require-ready] [--json]
|
|
1133
1184
|
filegrc prepare-audit <audit-id> [--json]
|
|
1134
1185
|
filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes] [--risk-level normal|high] [--json]
|
|
1135
1186
|
filegrc external-reviewer-setup --scaffold
|
|
@@ -1214,7 +1265,9 @@ workspaces migrate to v2 first. Continue one version at a time through v${ACTIVE
|
|
|
1214
1265
|
the repository Workspace, management Program, bounded Systems, operational Components,
|
|
1215
1266
|
specific Assets, Vendors, normalized information, and Evidence Artifacts. Model v5 separates
|
|
1216
1267
|
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.
|
|
1268
|
+
Training the same approval and activation split and moves its schedule into Obligations. Model v7
|
|
1269
|
+
keeps issued historical Documents neutral and requires a current activation for legacy Training. Model v8
|
|
1270
|
+
adds structured retention schedule items, reviewed requirement mappings, source-linked Commitments, and custom obligations. v3 migration
|
|
1218
1271
|
previews may require a decisions JSON file for ambiguous old Systems. v3 still creates planned
|
|
1219
1272
|
core Appointments, removal of obsolete manual page state, classified review work,
|
|
1220
1273
|
and dataModelVersion changed last. The command writes no Git commit.
|
|
@@ -1231,7 +1284,8 @@ Options:
|
|
|
1231
1284
|
--help Show this help
|
|
1232
1285
|
|
|
1233
1286
|
Start with:
|
|
1234
|
-
npx filegrc
|
|
1287
|
+
npx filegrc guide --json
|
|
1288
|
+
# Use the next model version shown by the guide.`);
|
|
1235
1289
|
return;
|
|
1236
1290
|
}
|
|
1237
1291
|
if (command === "program-readiness") {
|
|
@@ -1252,6 +1306,24 @@ Options:
|
|
|
1252
1306
|
--help Show this help`);
|
|
1253
1307
|
return;
|
|
1254
1308
|
}
|
|
1309
|
+
if (command === "program-amendment") {
|
|
1310
|
+
console.log(`Usage:
|
|
1311
|
+
filegrc program-amendment <source-resource-id> [--json]
|
|
1312
|
+
|
|
1313
|
+
Trace a Policy, Document, Framework, Requirement, or Commitment through its
|
|
1314
|
+
Commitments, mappings, Controls, Obligations, and retention rules. The command
|
|
1315
|
+
only reports review work. It does not infer or write management decisions.`);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (command === "review-bindings") {
|
|
1319
|
+
console.log(`Usage:
|
|
1320
|
+
filegrc review-bindings <retention-or-mapping-id> [--json]
|
|
1321
|
+
|
|
1322
|
+
Calculate the exact current source revisions required by an active Retention
|
|
1323
|
+
Schedule Item or Requirement Mapping. Copy reviewedSourceRevisions into the
|
|
1324
|
+
record only after management completes the review.`);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1255
1327
|
if (command === "workflow") {
|
|
1256
1328
|
console.log(`Usage:
|
|
1257
1329
|
filegrc workflow [audit-id] [options]
|
|
@@ -1398,6 +1470,10 @@ Options:
|
|
|
1398
1470
|
}
|
|
1399
1471
|
|
|
1400
1472
|
function agentOverview(model) {
|
|
1473
|
+
const currentModelIndex = SUPPORTED_MODEL_VERSIONS.indexOf(String(model.modelVersion));
|
|
1474
|
+
const nextModelVersion = currentModelIndex >= 0 && currentModelIndex < SUPPORTED_MODEL_VERSIONS.length - 1
|
|
1475
|
+
? SUPPORTED_MODEL_VERSIONS[currentModelIndex + 1]
|
|
1476
|
+
: ACTIVE_MODEL_VERSION;
|
|
1401
1477
|
const commands = {
|
|
1402
1478
|
help: "filegrc help",
|
|
1403
1479
|
version: "filegrc version",
|
|
@@ -1406,7 +1482,7 @@ function agentOverview(model) {
|
|
|
1406
1482
|
build: "filegrc build [root]",
|
|
1407
1483
|
validate: "filegrc validate [root] --json",
|
|
1408
1484
|
model: "filegrc model --json",
|
|
1409
|
-
migrate:
|
|
1485
|
+
migrate: `filegrc migrate --to-model ${nextModelVersion} --preview --json`,
|
|
1410
1486
|
describe: "filegrc describe <resource-type>",
|
|
1411
1487
|
types: "filegrc types --json",
|
|
1412
1488
|
guide: "filegrc guide [resource-type] --json",
|
|
@@ -1419,6 +1495,8 @@ function agentOverview(model) {
|
|
|
1419
1495
|
search: "filegrc search <query> --json",
|
|
1420
1496
|
obligations: "filegrc obligations --json",
|
|
1421
1497
|
programReadiness: "filegrc program-readiness --json",
|
|
1498
|
+
programAmendment: "filegrc program-amendment <source-resource-id> --json",
|
|
1499
|
+
reviewBindings: "filegrc review-bindings <retention-or-mapping-id> --json",
|
|
1422
1500
|
evidenceMap: "filegrc evidence-map --json",
|
|
1423
1501
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
1424
1502
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
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,
|
|
@@ -10,16 +10,36 @@ import { resolveProgram } from "./program.js";
|
|
|
10
10
|
import { markdownEntries } from "./resource-markdown.js";
|
|
11
11
|
|
|
12
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) {
|
|
13
31
|
const program = Object.hasOwn(options, "program")
|
|
14
32
|
? options.program
|
|
15
33
|
: resolveProgram(loaded, options.programId);
|
|
16
|
-
const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program)
|
|
34
|
+
const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program, { legacy })
|
|
17
35
|
.map((input) => [input.record.id, input]));
|
|
18
36
|
const authoritativeSource = loaded.resources.find(({ id }) => id === options.authoritativeSourceId);
|
|
19
37
|
if (authoritativeSource) {
|
|
20
38
|
inputs.set(authoritativeSource.id, {
|
|
21
39
|
record: authoritativeSource,
|
|
22
|
-
value:
|
|
40
|
+
value: legacy
|
|
41
|
+
? authoritativeSource
|
|
42
|
+
: authoritativeSourceRevisionValue(authoritativeSource),
|
|
23
43
|
includeContent: true
|
|
24
44
|
});
|
|
25
45
|
}
|
|
@@ -29,7 +49,7 @@ export function collectionRevision(loaded, resourceType, options = {}) {
|
|
|
29
49
|
revision: createHash("sha256")
|
|
30
50
|
.update(JSON.stringify(canonicalRecordValue(loaded.model, record.type, value)))
|
|
31
51
|
.digest("hex"),
|
|
32
|
-
contentRevisions: (includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
|
|
52
|
+
contentRevisions: (legacy || includeContent ? markdownEntries(loaded.model, record) : []).flatMap(({ path }) => {
|
|
33
53
|
try {
|
|
34
54
|
const content = readFileSync(resolveDataPath(loaded.root, path), "utf8");
|
|
35
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,7 +55,44 @@ 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
98
|
return reviewed.map((record) => ({ record, value: record, includeContent: true }));
|
|
@@ -75,6 +131,13 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
|
75
131
|
addIds((record.informationUses || []).map(({ informationTypeId }) => informationTypeId));
|
|
76
132
|
}
|
|
77
133
|
}
|
|
134
|
+
if (resourceType === "information-type" && !legacy) {
|
|
135
|
+
addIds(program?.systemIds);
|
|
136
|
+
addIds(programComponents(loaded, program || {}).map(({ id }) => id));
|
|
137
|
+
addIds(loaded.resources
|
|
138
|
+
.filter((record) => record.type === "vendor" && record.status !== "retired")
|
|
139
|
+
.map(({ id }) => id));
|
|
140
|
+
}
|
|
78
141
|
if (resourceType === "complementary-control") {
|
|
79
142
|
addIds(program?.systemIds);
|
|
80
143
|
addIds(program?.controlIds);
|
|
@@ -90,8 +153,9 @@ export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
|
90
153
|
record,
|
|
91
154
|
value: reviewedIds.has(record.id)
|
|
92
155
|
? record
|
|
93
|
-
: dependencyRevisionValue(resourceType, record),
|
|
94
|
-
includeContent:
|
|
156
|
+
: dependencyRevisionValue(resourceType, record, legacy),
|
|
157
|
+
includeContent: legacy
|
|
158
|
+
|| reviewedIds.has(record.id)
|
|
95
159
|
|| dependencyContentAffectsRevision(resourceType, record.type)
|
|
96
160
|
}));
|
|
97
161
|
}
|
|
@@ -167,11 +231,16 @@ const dependencyFields = {
|
|
|
167
231
|
},
|
|
168
232
|
component: {
|
|
169
233
|
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
170
|
-
control: ["id", "type", "
|
|
234
|
+
control: ["id", "type", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
171
235
|
vendor: ["id", "type", "status", "category", "criticality", "description", "startDate", "endDate"],
|
|
172
236
|
classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
|
|
173
237
|
"information-type": ["id", "type", "status", "classificationId", "description"]
|
|
174
238
|
},
|
|
239
|
+
"information-type": {
|
|
240
|
+
system: ["id", "type", "status", "informationTypeIds"],
|
|
241
|
+
component: ["id", "type", "status", "systemUses", "informationUses"],
|
|
242
|
+
vendor: ["id", "type", "status", "informationTypeIds"]
|
|
243
|
+
},
|
|
175
244
|
"complementary-control": {
|
|
176
245
|
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
177
246
|
control: ["id", "type", "status", "statement", "activity", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
@@ -183,6 +252,21 @@ const dependencyFields = {
|
|
|
183
252
|
}
|
|
184
253
|
};
|
|
185
254
|
|
|
255
|
+
const legacyDependencyFieldOverrides = {
|
|
256
|
+
framework: {
|
|
257
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"]
|
|
258
|
+
},
|
|
259
|
+
component: {
|
|
260
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
261
|
+
vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"]
|
|
262
|
+
},
|
|
263
|
+
"complementary-control": {
|
|
264
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
265
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
266
|
+
component: ["id", "type", "status", "componentKind", "description", "vendorId", "systemUses", "informationUses", "internetExposed"]
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
186
270
|
const dependencyContentTypes = {
|
|
187
271
|
framework: new Set(["system"]),
|
|
188
272
|
vendor: new Set(["document"]),
|
|
@@ -190,8 +274,11 @@ const dependencyContentTypes = {
|
|
|
190
274
|
"complementary-control": new Set(["system", "control", "document", "component"])
|
|
191
275
|
};
|
|
192
276
|
|
|
193
|
-
function dependencyRevisionValue(resourceType, record) {
|
|
194
|
-
const fields =
|
|
277
|
+
function dependencyRevisionValue(resourceType, record, legacy) {
|
|
278
|
+
const fields = legacy
|
|
279
|
+
? legacyDependencyFieldOverrides[resourceType]?.[record.type]
|
|
280
|
+
|| dependencyFields[resourceType]?.[record.type]
|
|
281
|
+
: dependencyFields[resourceType]?.[record.type];
|
|
195
282
|
if (!fields) return { id: record.id, type: record.type };
|
|
196
283
|
return Object.fromEntries(fields
|
|
197
284
|
.filter((field) => record[field] !== undefined)
|
|
@@ -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),
|
package/src/git.js
CHANGED
|
@@ -390,8 +390,8 @@ function unavailableSnapshot(error, extra = {}) {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
export async function getBrowserRepositoryState(input = process.cwd(), options = {}) {
|
|
393
|
-
const root = resolveWorkspaceRoot(input);
|
|
394
|
-
const config = await getRepositoryConfig(
|
|
393
|
+
const root = input?.entries && input?.root ? input.root : resolveWorkspaceRoot(input);
|
|
394
|
+
const config = await getRepositoryConfig(input);
|
|
395
395
|
const gitSummary = options.repositorySnapshot ?? await getRepositorySnapshot(root);
|
|
396
396
|
if (config.mode !== "trunk") {
|
|
397
397
|
return {
|
|
@@ -852,8 +852,8 @@ function syncReadySummary(root, action) {
|
|
|
852
852
|
return summary;
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
-
async function getRepositoryConfig(
|
|
856
|
-
const loaded = await loadWorkspace(
|
|
855
|
+
async function getRepositoryConfig(input) {
|
|
856
|
+
const loaded = input?.entries && input?.root ? input : await loadWorkspace(input);
|
|
857
857
|
const renderer = loaded.resources.find(({ type, id }) => type === "renderer-settings" && id === "renderer-settings");
|
|
858
858
|
const mode = renderer?.repositoryMode;
|
|
859
859
|
const authoritativeBranch = cleanGitName(renderer?.authoritativeBranch);
|
package/src/index.js
CHANGED
|
@@ -82,6 +82,7 @@ export {
|
|
|
82
82
|
setupExternalReviewerGovernance
|
|
83
83
|
} from "./external-reviewer.js";
|
|
84
84
|
export { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
85
|
+
export { planProgramAmendment } from "./program-amendment.js";
|
|
85
86
|
export {
|
|
86
87
|
buildAgentProgramPath,
|
|
87
88
|
PROGRAM_PATH,
|
|
@@ -99,6 +100,8 @@ export {
|
|
|
99
100
|
export { searchResources, searchableValues } from "./search.js";
|
|
100
101
|
export { effectiveResourceStatus } from "./resource-status.js";
|
|
101
102
|
export { applyReconciliation, planReconciliation } from "./reconciliation.js";
|
|
103
|
+
export { assessRequirementMappingReadiness } from "./requirement-mapping.js";
|
|
104
|
+
export { assessRetentionReadiness, nearDuplicateInformationTypes, resourceReviewRevision, resourceReviewRevisions, retentionReviewResourceIds, retentionRuleIsCurrent, retentionUses } from "./retention.js";
|
|
102
105
|
export { createFilegrcServer, serveWorkspace } from "./server.js";
|
|
103
106
|
export { normalizeSetupPayload, planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
104
107
|
export { createAppState, createResourceDetail } from "./state.js";
|