filegrc 0.14.0 → 0.15.1
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 +2 -1
- package/model/v11.json +5 -5
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/audit-preparation.js +16 -4
- package/src/audit-selection.js +33 -0
- package/src/cli.js +6 -3
- package/src/collection-review.js +61 -2
- package/src/collection-revision.js +3 -1
- package/src/collection-scope.js +45 -1
- package/src/index.js +1 -1
- package/src/program-path.js +59 -14
- package/src/program-readiness.js +217 -30
- package/src/retention-schedule-approval.js +121 -0
- package/src/retention.js +87 -12
- package/src/state.js +19 -22
- package/src/validate.js +46 -0
- package/src/web.js +509 -139
- package/src/workflow-history-integrity.js +5 -1
- package/src/workflow.js +2 -0
package/model/index.js
CHANGED
|
@@ -17,7 +17,8 @@ export const MODEL_CAPABILITY_VERSIONS = Object.freeze({
|
|
|
17
17
|
"temporal-collection-reviews": 9,
|
|
18
18
|
"reporting-routes": 9,
|
|
19
19
|
"reporting-route-sets": 10,
|
|
20
|
-
"control-collection-oversight": 11
|
|
20
|
+
"control-collection-oversight": 11,
|
|
21
|
+
"retention-schedule-approval": 11
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
export function modelSupports(modelOrVersion, capability) {
|
package/model/v11.json
CHANGED
|
@@ -643,12 +643,12 @@
|
|
|
643
643
|
]
|
|
644
644
|
},
|
|
645
645
|
"retention-schedule-item": {
|
|
646
|
-
"title": "Retention
|
|
647
|
-
"description": "
|
|
646
|
+
"title": "Data Retention Schedule",
|
|
647
|
+
"description": "Approve the schedule document and its structured rows as one governed schedule. Include only actual information uses and keep undecided organization values planned.",
|
|
648
648
|
"reviewPoints": [
|
|
649
|
-
"
|
|
650
|
-
"Confirm
|
|
651
|
-
"
|
|
649
|
+
"Review the Data Retention Schedule document, its Markdown, and every included structured row as one revision.",
|
|
650
|
+
"Confirm schedule coverage across the Systems, Components, Vendors, source families, logs, backups, audit records, and FileGRC records the organization actually uses.",
|
|
651
|
+
"Confirm every active item has an approved cutoff, retention period, disposition action, owner, approver, and exact reviewed source revisions; keep undecided behavior planned."
|
|
652
652
|
],
|
|
653
653
|
"decisions": [
|
|
654
654
|
"complete",
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -82,7 +82,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
82
82
|
instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
|
|
83
83
|
use: definition.description,
|
|
84
84
|
purpose: definition.description,
|
|
85
|
-
programStep: resourceProgramContext(type),
|
|
85
|
+
programStep: resourceProgramContext(type, loaded.model),
|
|
86
86
|
policyBasis: definition.guidance.policyBasis,
|
|
87
87
|
cadence: definition.guidance.cadence,
|
|
88
88
|
emptyState: definition.guidance.emptyState ?? null,
|
package/src/audit-preparation.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { modelSupports } from "../model/index.js";
|
|
4
4
|
import { applicabilityReviewIsCurrent } from "./applicability-scope.js";
|
|
5
|
+
import { selectDefaultAudit } from "./audit-selection.js";
|
|
5
6
|
import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
|
|
6
7
|
import {
|
|
7
8
|
coverageContains,
|
|
@@ -63,20 +64,21 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
63
64
|
const records = loaded.resources;
|
|
64
65
|
const byId = new Map(records.map((record) => [record.id, record]));
|
|
65
66
|
const audits = records.filter((record) => record.type === "audit");
|
|
67
|
+
const calendarAsOf = options.asOf || currentCalendarDate(loaded.workspace?.timezone || "UTC");
|
|
66
68
|
const audit = options.auditId
|
|
67
69
|
? audits.find((record) => record.id === options.auditId)
|
|
68
70
|
: options.selectDefault === false
|
|
69
71
|
? null
|
|
70
|
-
: audits
|
|
72
|
+
: selectDefaultAudit(audits, calendarAsOf);
|
|
71
73
|
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
72
74
|
|
|
73
|
-
const calendarAsOf = options.asOf || currentCalendarDate(loaded.workspace?.timezone || "UTC");
|
|
74
75
|
const formalPeriodEnd = audit?.auditKind === "soc-2-type-2" ? coverageEnd(audit.coverage) : null;
|
|
75
76
|
const readinessAsOf = formalPeriodEnd && formalPeriodEnd < calendarAsOf ? formalPeriodEnd : calendarAsOf;
|
|
76
77
|
const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
|
|
77
78
|
asOf: readinessAsOf,
|
|
78
79
|
generatedAt: options.generatedAt,
|
|
79
|
-
programId: audit?.programId || options.programId
|
|
80
|
+
programId: audit?.programId || options.programId,
|
|
81
|
+
auditId: audit?.id
|
|
80
82
|
});
|
|
81
83
|
const documentActivations = audit && modelSupports(loaded.model, "governed-document-activation")
|
|
82
84
|
? await auditDocumentActivationAssessments(loaded, audit, byId, calendarAsOf)
|
|
@@ -746,6 +748,12 @@ function canonicalScopeValue(value) {
|
|
|
746
748
|
function programFoundationStage(programReadiness, workspace, audit) {
|
|
747
749
|
const needsOperatingPeriod = audit?.auditKind === "soc-2-type-2";
|
|
748
750
|
const ready = needsOperatingPeriod ? programReadiness.operating : programReadiness.evidenceReady;
|
|
751
|
+
const setupRemaining = programReadiness.setupProgress?.remaining
|
|
752
|
+
?? (programReadiness.progress?.mode === "setup-progress" ? programReadiness.progress.remaining : 0);
|
|
753
|
+
const activeWindow = programReadiness.progress?.mode === "operating-window";
|
|
754
|
+
const activeWindowLabel = programReadiness.progress?.source === "audit"
|
|
755
|
+
? "audit window"
|
|
756
|
+
: "candidate operating period";
|
|
749
757
|
return stage("program", "Program Readiness", "The management program can be prepared and operated without an audit record or CPA firm.", [
|
|
750
758
|
item(
|
|
751
759
|
"evidence-ready",
|
|
@@ -755,9 +763,13 @@ function programFoundationStage(programReadiness, workspace, audit) {
|
|
|
755
763
|
? needsOperatingPeriod
|
|
756
764
|
? `${programReadiness.target.label} is operating for the selected engagement.`
|
|
757
765
|
: `${programReadiness.target.label} is evidence-ready.`
|
|
766
|
+
: needsOperatingPeriod && activeWindow
|
|
767
|
+
? setupRemaining
|
|
768
|
+
? `The ${activeWindowLabel} is active, but ${setupRemaining} required setup ${setupRemaining === 1 ? "action remains" : "actions remain"} across scope, policies, controls, and evidence preparation.`
|
|
769
|
+
: `The ${activeWindowLabel} is active, but current operating work needs attention before treating the engagement as management-ready.`
|
|
758
770
|
: needsOperatingPeriod && programReadiness.evidenceReady
|
|
759
771
|
? "Start the candidate period and finish the current operating-readiness work before treating a Type 2 engagement as management-ready."
|
|
760
|
-
: `${
|
|
772
|
+
: `${setupRemaining} required setup ${setupRemaining === 1 ? "action remains" : "actions remain"} across scope, policies, controls, and evidence preparation.`,
|
|
761
773
|
workspace || { type: "workspace" }
|
|
762
774
|
)
|
|
763
775
|
]);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function selectDefaultAudit(audits = [], asOf = "") {
|
|
2
|
+
const terminal = new Set(["complete", "closed", "canceled", "delivered"]);
|
|
3
|
+
const coverageStart = (audit) => audit?.coverage?.startsOn
|
|
4
|
+
|| audit?.coverage?.periodStart
|
|
5
|
+
|| audit?.coverage?.on
|
|
6
|
+
|| "";
|
|
7
|
+
const statusOrder = new Map([
|
|
8
|
+
["planned", 0],
|
|
9
|
+
["in-progress", 1],
|
|
10
|
+
["fieldwork", 2],
|
|
11
|
+
["report-draft", 3],
|
|
12
|
+
["issued", 4],
|
|
13
|
+
["delivered", 5],
|
|
14
|
+
["complete", 6]
|
|
15
|
+
]);
|
|
16
|
+
const open = audits.filter((audit) => !terminal.has(audit.status));
|
|
17
|
+
const candidates = [...(open.length ? open : audits)];
|
|
18
|
+
candidates.sort((left, right) => {
|
|
19
|
+
const leftStart = coverageStart(left);
|
|
20
|
+
const rightStart = coverageStart(right);
|
|
21
|
+
const leftStarted = Boolean(leftStart && (!asOf || leftStart <= asOf));
|
|
22
|
+
const rightStarted = Boolean(rightStart && (!asOf || rightStart <= asOf));
|
|
23
|
+
return Number(rightStarted) - Number(leftStarted)
|
|
24
|
+
|| (leftStarted && rightStarted
|
|
25
|
+
? rightStart.localeCompare(leftStart)
|
|
26
|
+
: leftStart && rightStart
|
|
27
|
+
? leftStart.localeCompare(rightStart)
|
|
28
|
+
: Number(Boolean(rightStart)) - Number(Boolean(leftStart)))
|
|
29
|
+
|| (statusOrder.get(right.status) ?? -1) - (statusOrder.get(left.status) ?? -1)
|
|
30
|
+
|| String(left.id || "").localeCompare(String(right.id || ""));
|
|
31
|
+
});
|
|
32
|
+
return candidates[0] || null;
|
|
33
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -447,16 +447,17 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
447
447
|
const loaded = await loadWorkspace(root);
|
|
448
448
|
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
449
449
|
const output = flags.summary ? summarizeProgramReadiness(result) : result;
|
|
450
|
+
const headlineStatus = result.progress.status || result.status;
|
|
450
451
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
451
452
|
else if (flags.summary) {
|
|
452
|
-
console.log(`${
|
|
453
|
+
console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
|
|
453
454
|
for (const stage of output.stages) {
|
|
454
|
-
console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action}
|
|
455
|
+
console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} checks need action`);
|
|
455
456
|
}
|
|
456
457
|
if (output.firstAction) console.log(`Next: ${output.firstAction.title}\t${output.firstAction.message}`);
|
|
457
458
|
}
|
|
458
459
|
else {
|
|
459
|
-
console.log(`${
|
|
460
|
+
console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
|
|
460
461
|
console.log(
|
|
461
462
|
`${result.target.label}`
|
|
462
463
|
+ (result.target.candidateCoverage?.kind === "range"
|
|
@@ -2036,6 +2037,8 @@ function summarizeProgramReadiness(result) {
|
|
|
2036
2037
|
suggestedCandidatePeriodStart: result.suggestedCandidatePeriodStart,
|
|
2037
2038
|
target: result.target,
|
|
2038
2039
|
progress: result.progress,
|
|
2040
|
+
setupProgress: result.setupProgress,
|
|
2041
|
+
checkProgress: result.checkProgress,
|
|
2039
2042
|
counts: result.counts,
|
|
2040
2043
|
scopeCounts: Object.fromEntries(
|
|
2041
2044
|
Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
|
package/src/collection-review.js
CHANGED
|
@@ -12,6 +12,7 @@ import { loadWorkspace } from "./workspace.js";
|
|
|
12
12
|
import { resolveProgram } from "./program.js";
|
|
13
13
|
import { currentCalendarDate } from "./time.js";
|
|
14
14
|
import { currentPartyPeople } from "./parties.js";
|
|
15
|
+
import { retentionScheduleApprovalIssues } from "./retention-schedule-approval.js";
|
|
15
16
|
|
|
16
17
|
export { collectionRevision };
|
|
17
18
|
|
|
@@ -66,7 +67,14 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
|
66
67
|
);
|
|
67
68
|
const reviewerEligibility = resourceType === "control"
|
|
68
69
|
? assessControlReviewers(loaded, records)
|
|
69
|
-
:
|
|
70
|
+
: resourceType === "retention-schedule-item" && modelSupports(loaded.model, "retention-schedule-approval")
|
|
71
|
+
? assessRetentionScheduleReviewers(loaded, records)
|
|
72
|
+
: null;
|
|
73
|
+
const approvalIssues = resourceType === "retention-schedule-item" && modelSupports(loaded.model, "retention-schedule-approval")
|
|
74
|
+
? retentionScheduleApprovalIssues(loaded, program, records, {
|
|
75
|
+
informationTypesReviewed: assessCollectionReview(loaded, "information-type", options).complete
|
|
76
|
+
})
|
|
77
|
+
: [];
|
|
70
78
|
const reviewersEligible = !reviewerEligibility || Boolean(
|
|
71
79
|
review?.reviewedByIds?.length
|
|
72
80
|
&& review.reviewedByIds.every((id) => reviewerEligibility.eligibleReviewerIds.includes(id))
|
|
@@ -77,6 +85,7 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
|
77
85
|
&& revisionMatches
|
|
78
86
|
&& temporalReview
|
|
79
87
|
&& reviewersEligible
|
|
88
|
+
&& !approvalIssues.length
|
|
80
89
|
);
|
|
81
90
|
const stale = Boolean(
|
|
82
91
|
review?.status === "active"
|
|
@@ -96,10 +105,15 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
|
96
105
|
status: complete ? "current" : stale ? "stale" : "review-required",
|
|
97
106
|
complete,
|
|
98
107
|
...(reviewerEligibility || {}),
|
|
108
|
+
...(approvalIssues.length ? { approvalIssues } : {}),
|
|
99
109
|
message: complete
|
|
100
110
|
? `${configuration.title} were reviewed on ${review.reviewedOn}.`
|
|
111
|
+
: approvalIssues.length
|
|
112
|
+
? approvalIssues[0].message
|
|
101
113
|
: revisionMatches && review?.status === "active" && !reviewersEligible
|
|
102
|
-
?
|
|
114
|
+
? resourceType === "retention-schedule-item"
|
|
115
|
+
? "Review the Data Retention Schedule again with a reviewer who does not own its governing document or an included schedule row."
|
|
116
|
+
: "Review the Control collection again with a reviewer who does not own an included Control or its enabled Obligation."
|
|
103
117
|
: stale
|
|
104
118
|
? `${configuration.title} changed after the last confirmation. Review the current records again.`
|
|
105
119
|
: !records.length && !allowsEmptyCollection
|
|
@@ -143,6 +157,9 @@ export async function scaffoldCollectionReview(input = process.cwd(), options =
|
|
|
143
157
|
reviewedOn: priorReview?.reviewedOn
|
|
144
158
|
? currentCalendarDate(loaded.workspace.timezone, now)
|
|
145
159
|
: null,
|
|
160
|
+
...(resourceType === "retention-schedule-item" && modelSupports(loaded.model, "retention-schedule-approval")
|
|
161
|
+
? { expectedCollectionRevision: assessment.collectionRevision }
|
|
162
|
+
: {}),
|
|
146
163
|
...(v4
|
|
147
164
|
? { authoritativeComponentId: preservedAuthoritativeSourceId }
|
|
148
165
|
: { authoritativeSystemId: preservedAuthoritativeSourceId })
|
|
@@ -154,7 +171,14 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
154
171
|
const program = resolveProgram(loaded, options.programId);
|
|
155
172
|
const resourceType = requiredType(loaded, options.resourceType);
|
|
156
173
|
const assessment = assessCollectionReview(loaded, resourceType, { programId: program.id });
|
|
174
|
+
if (options.expectedCollectionRevision && options.expectedCollectionRevision !== assessment.collectionRevision) {
|
|
175
|
+
throw new Error(`${assessment.configuration.title} changed after it was displayed. Reload and review the current revision before approving it.`);
|
|
176
|
+
}
|
|
177
|
+
if (resourceType === "retention-schedule-item" && modelSupports(loaded.model, "retention-schedule-approval") && !options.expectedCollectionRevision) {
|
|
178
|
+
throw new Error("Data Retention Schedule approval requires the displayed collection revision. Reload or scaffold the current review before approving it.");
|
|
179
|
+
}
|
|
157
180
|
await requireControlReviewReady(loaded, resourceType, program.id);
|
|
181
|
+
await requireRetentionScheduleReady(loaded, resourceType, program);
|
|
158
182
|
const configuration = assessment.configuration;
|
|
159
183
|
const decision = String(options.decision || "").trim();
|
|
160
184
|
const rationale = String(options.rationale || "").trim();
|
|
@@ -199,6 +223,13 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
199
223
|
throw new Error(`Control collection review needs a reviewer who does not own an included Control or its enabled Obligation: ${conflicts.join(", ")}.`);
|
|
200
224
|
}
|
|
201
225
|
}
|
|
226
|
+
if (resourceType === "retention-schedule-item" && modelSupports(loaded.model, "retention-schedule-approval")) {
|
|
227
|
+
const eligible = new Set(assessment.eligibleReviewerIds || []);
|
|
228
|
+
const conflicts = reviewedByIds.filter((id) => !eligible.has(id));
|
|
229
|
+
if (conflicts.length) {
|
|
230
|
+
throw new Error(`Data Retention Schedule approval needs a reviewer who does not own its governing document or an included schedule row: ${conflicts.join(", ")}.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
202
233
|
if (temporalReviews) {
|
|
203
234
|
if (reviewedOn !== today) {
|
|
204
235
|
throw new Error("A current Collection Review must use today's workspace-local review date. Reconstruct an older population from Git history instead of backdating today's files.");
|
|
@@ -308,6 +339,15 @@ async function requireControlReviewReady(loaded, resourceType, programId) {
|
|
|
308
339
|
}
|
|
309
340
|
}
|
|
310
341
|
|
|
342
|
+
async function requireRetentionScheduleReady(loaded, resourceType, program) {
|
|
343
|
+
if (resourceType !== "retention-schedule-item" || !modelSupports(loaded.model, "retention-schedule-approval")) return;
|
|
344
|
+
const rows = scopedCollectionRecords(loaded, resourceType, program);
|
|
345
|
+
const approvalIssues = retentionScheduleApprovalIssues(loaded, program, rows, {
|
|
346
|
+
informationTypesReviewed: assessCollectionReview(loaded, "information-type", { programId: program.id }).complete
|
|
347
|
+
});
|
|
348
|
+
if (approvalIssues.length) throw new Error(approvalIssues[0].message);
|
|
349
|
+
}
|
|
350
|
+
|
|
311
351
|
function assessControlReviewers(loaded, controls) {
|
|
312
352
|
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
313
353
|
const controlIds = new Set(controls.map(({ id }) => id));
|
|
@@ -329,6 +369,25 @@ function assessControlReviewers(loaded, controls) {
|
|
|
329
369
|
};
|
|
330
370
|
}
|
|
331
371
|
|
|
372
|
+
function assessRetentionScheduleReviewers(loaded, rows) {
|
|
373
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
374
|
+
const conflictIds = new Set();
|
|
375
|
+
const schedules = loaded.resources.filter((record) => (
|
|
376
|
+
record.type === "document"
|
|
377
|
+
&& record.documentKind === "schedule"
|
|
378
|
+
&& record.workflowScope === "program"
|
|
379
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
380
|
+
));
|
|
381
|
+
for (const record of [...schedules, ...rows]) {
|
|
382
|
+
for (const id of currentPartyPeople(record.ownerIds || [], byId)) conflictIds.add(id);
|
|
383
|
+
}
|
|
384
|
+
const people = loaded.resources.filter(({ type, status }) => type === "person" && status === "active");
|
|
385
|
+
return {
|
|
386
|
+
eligibleReviewerIds: people.map(({ id }) => id).filter((id) => !conflictIds.has(id)),
|
|
387
|
+
reviewerConflictIds: people.map(({ id }) => id).filter((id) => conflictIds.has(id))
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
332
391
|
function sameIds(left, right) {
|
|
333
392
|
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
334
393
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { modelSupports } from "../model/index.js";
|
|
3
4
|
import {
|
|
4
5
|
authoritativeSourceRevisionValue,
|
|
5
6
|
collectionRevisionInputs,
|
|
@@ -24,7 +25,8 @@ export function collectionRevisionMatches(loaded, resourceType, storedRevision,
|
|
|
24
25
|
// Version 0.9.2 narrowed this hash basis. Keep unchanged 0.9.1 reviews valid
|
|
25
26
|
// until management records a new review on the current basis.
|
|
26
27
|
return storedRevision === currentRevision
|
|
27
|
-
||
|
|
28
|
+
|| (resourceType !== "retention-schedule-item" || !modelSupports(loaded.model, "retention-schedule-approval"))
|
|
29
|
+
&& storedRevision === legacyCollectionRevision(loaded, resourceType, options);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function calculateCollectionRevision(loaded, resourceType, options, legacy) {
|
package/src/collection-scope.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { modelSupports } from "../model/index.js";
|
|
2
2
|
import { programComponents, selectedRequirementIds } from "./program.js";
|
|
3
|
-
import { retentionReviewResourceIds } from "./retention.js";
|
|
3
|
+
import { retentionReviewResourceIds, retentionUses } from "./retention.js";
|
|
4
4
|
import { documentIsAuditSpecific } from "./program-lifecycle.js";
|
|
5
5
|
|
|
6
6
|
export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
7
7
|
if (!modelSupports(loaded.model, "program-scope")) {
|
|
8
8
|
return loaded.resources.filter((record) => record.type === resourceType);
|
|
9
9
|
}
|
|
10
|
+
if (resourceType === "retention-schedule-item" && modelSupports(loaded.model, "retention-schedule-approval")) {
|
|
11
|
+
return loaded.resources.filter((record) => (
|
|
12
|
+
record.type === resourceType && !["retired", "superseded"].includes(record.status)
|
|
13
|
+
));
|
|
14
|
+
}
|
|
10
15
|
if (resourceType === "person") {
|
|
11
16
|
return scopedProgramPeople(loaded, program);
|
|
12
17
|
}
|
|
@@ -225,6 +230,29 @@ export function collectionRevisionInputs(loaded, resourceType, program, options
|
|
|
225
230
|
.filter((record) => record.type === "vendor" && record.status !== "retired")
|
|
226
231
|
.map(({ id }) => id));
|
|
227
232
|
}
|
|
233
|
+
if (resourceType === "retention-schedule-item" && !legacy && modelSupports(loaded.model, "retention-schedule-approval")) {
|
|
234
|
+
const uses = retentionUses(loaded, program || {});
|
|
235
|
+
addIds(uses.map(({ resource }) => resource.id));
|
|
236
|
+
addIds(uses.map(({ informationTypeId }) => informationTypeId));
|
|
237
|
+
const schedules = loaded.resources
|
|
238
|
+
.filter((record) => (
|
|
239
|
+
record.type === "document"
|
|
240
|
+
&& record.documentKind === "schedule"
|
|
241
|
+
&& record.workflowScope === "program"
|
|
242
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
243
|
+
))
|
|
244
|
+
addIds(schedules.map(({ id }) => id));
|
|
245
|
+
for (const schedule of schedules) {
|
|
246
|
+
addPartyIds([...(schedule.ownerIds || []), ...(schedule.approverIds || [])]);
|
|
247
|
+
}
|
|
248
|
+
for (const record of reviewed) {
|
|
249
|
+
addIds([record.scheduleDocumentId]);
|
|
250
|
+
addIds(record.informationTypeIds);
|
|
251
|
+
addIds(record.scopeResourceIds);
|
|
252
|
+
addIds(record.sourceResourceIds);
|
|
253
|
+
addPartyIds([...(record.ownerIds || []), ...(record.approvedByIds || [])]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
228
256
|
if (resourceType === "complementary-control") {
|
|
229
257
|
addIds(program?.systemIds);
|
|
230
258
|
addIds(program?.controlIds);
|
|
@@ -351,6 +379,21 @@ const dependencyFields = {
|
|
|
351
379
|
component: ["id", "type", "status", "systemUses", "informationUses"],
|
|
352
380
|
vendor: ["id", "type", "status", "informationTypeIds"]
|
|
353
381
|
},
|
|
382
|
+
"retention-schedule-item": {
|
|
383
|
+
document: ["id", "type", "documentKind", "workflowScope", "ownerIds", "approverIds", "approvedOn", "approvedContentRevisions", "controlIds"],
|
|
384
|
+
policy: ["id", "type", "status", "programRole", "ownerIds", "approverIds", "approvedOn", "approvedContentRevisions", "controlIds"],
|
|
385
|
+
requirement: ["id", "type", "title", "frameworkId", "reference", "description"],
|
|
386
|
+
commitment: ["id", "type", "status", "commitmentKind", "statement", "effectiveOn"],
|
|
387
|
+
control: ["id", "type", "status", "statement", "systemIds", "componentIds"],
|
|
388
|
+
"information-type": ["id", "type", "status", "classificationId", "description"],
|
|
389
|
+
system: ["id", "type", "status", "informationTypeIds"],
|
|
390
|
+
component: ["id", "type", "status", "systemUses", "informationUses"],
|
|
391
|
+
vendor: ["id", "type", "status", "informationTypeIds"],
|
|
392
|
+
"source-coverage": ["id", "type", "status", "sourceFamilyId", "coverageKind", "scopeResourceIds", "retentionScheduleItemIds", "componentId"],
|
|
393
|
+
person: ["id", "type", "status", "affiliation", "jobTitle"],
|
|
394
|
+
team: ["id", "type", "status", "memberIds", "chairIds"],
|
|
395
|
+
appointment: ["id", "type", "status", "appointmentKind", "scopeResourceIds", "holderId", "startsOn", "endsOn"]
|
|
396
|
+
},
|
|
354
397
|
"complementary-control": {
|
|
355
398
|
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
356
399
|
control: ["id", "type", "status", "statement", "activity", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
@@ -382,6 +425,7 @@ const dependencyContentTypes = {
|
|
|
382
425
|
vendor: new Set(["document"]),
|
|
383
426
|
component: new Set(["system"]),
|
|
384
427
|
control: new Set(["system", "component", "obligation", "source-coverage", "evidence", "retention-schedule-item", "document", "policy", "training"]),
|
|
428
|
+
"retention-schedule-item": new Set(["document", "policy"]),
|
|
385
429
|
"complementary-control": new Set(["system", "control", "document", "component"])
|
|
386
430
|
};
|
|
387
431
|
|
package/src/index.js
CHANGED
|
@@ -86,7 +86,7 @@ export {
|
|
|
86
86
|
scaffoldExternalReviewerGovernance,
|
|
87
87
|
setupExternalReviewerGovernance
|
|
88
88
|
} from "./external-reviewer.js";
|
|
89
|
-
export { assessEvidenceMap, assessProgramReadiness } from "./program-readiness.js";
|
|
89
|
+
export { assessEvidenceMap, assessProgramReadiness, calculateProgramProgress } from "./program-readiness.js";
|
|
90
90
|
export {
|
|
91
91
|
approveReportingRouteSet,
|
|
92
92
|
assessReportingRoutePeriod,
|
package/src/program-path.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ACTIVE_MODEL_VERSION, modelSupports } from "../model/index.js";
|
|
2
|
+
|
|
1
3
|
export const RESOURCE_INSTRUCTIONS = {
|
|
2
4
|
program: "Define one management compliance or assurance Program with its goal, bounded Systems, selected Frameworks, Requirement applicability decisions, Controls, owners, risk method, and candidate period.",
|
|
3
5
|
person: "Record each person’s actual organizational job title. Keep named program authority, such as CISO, DPO, Policy Owner, or team chair, in dated Appointment records.",
|
|
@@ -105,13 +107,14 @@ export const PROGRAM_PATH = [
|
|
|
105
107
|
id: "policies",
|
|
106
108
|
number: 2,
|
|
107
109
|
title: "Approve Policies",
|
|
108
|
-
description: "
|
|
109
|
-
summary: "Review and independently approve Policies, program Documents, and
|
|
110
|
+
description: "Approve governed content and retention decisions",
|
|
111
|
+
summary: "Review and independently approve Policies, program Documents, Training content, and the Data Retention Schedule.",
|
|
110
112
|
sections: [
|
|
111
|
-
{ id: "policy-content", title: "Policies", description: "Review every program Policy, Document, and Training record in one table, then bind independent approval to each exact revision.", steps: ["Review each governed Markdown artifact and replace every organization placeholder.", "Confirm the owner, separate approver, linked Controls, audience, and intended values that apply to each artifact.", "Record approval and its date against the exact revision. Leave approved content inactive until its Step 3 implementation cutover."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/
|
|
113
|
+
{ id: "policy-content", title: "Policies", description: "Review every program Policy, Document, and Training record in one table, then bind independent approval to each exact revision.", steps: ["Review each governed Markdown artifact and replace every organization placeholder.", "Confirm the owner, separate approver, linked Controls, audience, and intended values that apply to each artifact.", "Record approval and its date against the exact revision. Leave approved content inactive until its Step 3 implementation cutover."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/policies" }], defaultOpen: true },
|
|
114
|
+
{ id: "retention", title: "Data Retention Schedule", description: "Approve the schedule document and its structured rows as one governed schedule.", steps: ["Review the schedule policy, legal holds, exceptions, and disposal requirements.", "Define rows only for Information Types and operational sources the organization actually uses.", "Confirm the complete document-and-row revision with one schedule review."], types: [], utility: "retention-schedule", defaultOpen: true }
|
|
112
115
|
],
|
|
113
116
|
resourceTypes: [],
|
|
114
|
-
supportingResourceTypes: ["policy", "document", "training"],
|
|
117
|
+
supportingResourceTypes: ["policy", "document", "training", "retention-schedule-item"],
|
|
115
118
|
commands: [
|
|
116
119
|
"filegrc guide policy --json",
|
|
117
120
|
"filegrc guide document --json",
|
|
@@ -119,30 +122,29 @@ export const PROGRAM_PATH = [
|
|
|
119
122
|
"filegrc list policy --json",
|
|
120
123
|
"filegrc list document --json",
|
|
121
124
|
"filegrc list training --json",
|
|
122
|
-
"filegrc get POLICY_ID --mutation"
|
|
125
|
+
"filegrc get POLICY_ID --mutation",
|
|
126
|
+
"filegrc guide retention-schedule-item --json",
|
|
127
|
+
"filegrc review-collection retention-schedule-item --scaffold"
|
|
123
128
|
]
|
|
124
129
|
},
|
|
125
130
|
{
|
|
126
131
|
id: "controls",
|
|
127
132
|
number: 3,
|
|
128
133
|
title: "Implement Controls",
|
|
129
|
-
description: "
|
|
130
|
-
summary: "
|
|
134
|
+
description: "Implement and prepare to operate",
|
|
135
|
+
summary: "Implement Controls, configure their operating schedules and evidence sources, then activate the approved program.",
|
|
131
136
|
sections: [
|
|
132
137
|
{ id: "controls", title: "Controls", description: "Implement the selected Controls and connect their scope, procedures, mappings, and operation patterns.", steps: ["Open every planned Control and confirm its mappings and operation pattern.", "Write the real procedure in Record Markdown, add bounded System scope, and map the operating and authoritative evidence-source Components."], types: ["control"], defaultOpen: true },
|
|
133
138
|
{ id: "complementary-controls", title: "Complementary Controls", description: "Record customer or provider responsibilities that form part of the implementation boundary, or confirm there are none.", steps: ["Review whether an in-scope Control depends on a customer or carved-out provider action."], types: ["complementary-control"], defaultOpen: true },
|
|
134
139
|
{ id: "evidence-sources", title: "Evidence Sources", description: "Check that every Control points to an authoritative source that can produce its expected evidence.", steps: ["Confirm each source Component is active, has the right evidence-source role, names current access owners, and includes repeatable retrieval instructions."], types: [], utility: "evidence-sources", defaultOpen: true },
|
|
135
|
-
{ id: "retention", title: "Retention Schedule", description: "Review each retention rule against current information uses and approved sources.", steps: ["Confirm scope, cutoff, period, disposition, sources, owner, and approval for each rule."], types: ["retention-schedule-item"], defaultOpen: true },
|
|
136
140
|
{ id: "obligations", title: "Obligations", description: "Configure the calendar and event schedules that operate the Controls.", steps: ["Review and enable every required calendar and event schedule."], types: ["obligation"], defaultOpen: true }
|
|
137
141
|
],
|
|
138
|
-
resourceTypes: ["control", "complementary-control", "
|
|
142
|
+
resourceTypes: ["control", "complementary-control", "obligation"],
|
|
139
143
|
commands: [
|
|
140
144
|
"filegrc guide control --json",
|
|
141
145
|
"filegrc list control --json",
|
|
142
146
|
"filegrc get CONTROL_ID --mutation",
|
|
143
147
|
"filegrc guide obligation --json",
|
|
144
|
-
"filegrc guide retention-schedule-item --json",
|
|
145
|
-
"filegrc review-collection retention-schedule-item --scaffold",
|
|
146
148
|
"filegrc list obligation --json",
|
|
147
149
|
"filegrc review-collection component --scaffold",
|
|
148
150
|
"filegrc review-collection complementary-control --scaffold",
|
|
@@ -264,8 +266,51 @@ export const PROGRAM_PATH = [
|
|
|
264
266
|
}
|
|
265
267
|
];
|
|
266
268
|
|
|
267
|
-
export function
|
|
269
|
+
export function programPathForModel(model = ACTIVE_MODEL_VERSION) {
|
|
270
|
+
if (modelSupports(model, "retention-schedule-approval")) return PROGRAM_PATH;
|
|
268
271
|
return PROGRAM_PATH.map((stage) => {
|
|
272
|
+
if (stage.id === "policies") {
|
|
273
|
+
return {
|
|
274
|
+
...stage,
|
|
275
|
+
description: "Review governed content and approvals",
|
|
276
|
+
summary: "Review and independently approve Policies, program Documents, and Training content.",
|
|
277
|
+
sections: [{
|
|
278
|
+
...stage.sections[0],
|
|
279
|
+
relatedLinks: [{ type: "policy", label: "Policies", href: "#/stage/policies" }]
|
|
280
|
+
}],
|
|
281
|
+
supportingResourceTypes: stage.supportingResourceTypes.filter((type) => type !== "retention-schedule-item"),
|
|
282
|
+
commands: stage.commands.filter((command) => !command.includes("retention-schedule-item"))
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (stage.id === "controls") {
|
|
286
|
+
const retention = {
|
|
287
|
+
id: "retention",
|
|
288
|
+
title: "Retention Schedule",
|
|
289
|
+
description: "Review each retention rule against current information uses and approved sources.",
|
|
290
|
+
steps: ["Confirm scope, cutoff, period, disposition, sources, owner, and approval for each rule."],
|
|
291
|
+
types: ["retention-schedule-item"],
|
|
292
|
+
defaultOpen: true
|
|
293
|
+
};
|
|
294
|
+
return {
|
|
295
|
+
...stage,
|
|
296
|
+
description: "Finish controls and their evidence sources",
|
|
297
|
+
summary: "Describe each Control and connect its evidence source.",
|
|
298
|
+
sections: [...stage.sections.slice(0, 3), retention, ...stage.sections.slice(3)],
|
|
299
|
+
resourceTypes: [...stage.resourceTypes.slice(0, 2), "retention-schedule-item", ...stage.resourceTypes.slice(2)],
|
|
300
|
+
commands: [
|
|
301
|
+
...stage.commands.slice(0, 5),
|
|
302
|
+
"filegrc guide retention-schedule-item --json",
|
|
303
|
+
"filegrc review-collection retention-schedule-item --scaffold",
|
|
304
|
+
...stage.commands.slice(5)
|
|
305
|
+
]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return stage;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function buildAgentProgramPath(model) {
|
|
313
|
+
return programPathForModel(model).map((stage) => {
|
|
269
314
|
const programResourceTypes = [...stage.resourceTypes, ...(stage.supportingResourceTypes || [])]
|
|
270
315
|
.filter((type) => model.resources[type]);
|
|
271
316
|
const resourcePages = programResourceTypes.map((type, index) => {
|
|
@@ -305,8 +350,8 @@ function agentCommand(command) {
|
|
|
305
350
|
return command.startsWith("filegrc ") ? `npx ${command}` : command;
|
|
306
351
|
}
|
|
307
352
|
|
|
308
|
-
export function resourceProgramContext(type) {
|
|
309
|
-
const stage =
|
|
353
|
+
export function resourceProgramContext(type, model) {
|
|
354
|
+
const stage = programPathForModel(model).find((candidate) => (
|
|
310
355
|
candidate.resourceTypes.includes(type) || (candidate.supportingResourceTypes || []).includes(type)
|
|
311
356
|
));
|
|
312
357
|
if (!stage) return null;
|