filegrc 0.8.0 → 0.9.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/README.md +7 -7
- package/model/index.js +6 -5
- package/model/v6.json +10358 -0
- package/package.json +1 -1
- package/src/cli.js +75 -6
- package/src/document-activation.js +61 -25
- package/src/files.js +54 -25
- package/src/index.js +8 -1
- package/src/model-migration.js +148 -3
- package/src/obligations.js +5 -5
- package/src/policy-library/information-security-policy-v2.md +2 -2
- package/src/policy-library.js +42 -10
- package/src/program-lifecycle.js +41 -3
- package/src/program-path.js +19 -18
- package/src/program-readiness.js +131 -16
- package/src/server.js +8 -1
- package/src/setup.js +1 -1
- package/src/validate.js +33 -5
- package/src/web.js +128 -26
- package/src/workflow.js +21 -4
package/src/model-migration.js
CHANGED
|
@@ -891,7 +891,11 @@ export async function planModelMigration(input = process.cwd(), options = {}) {
|
|
|
891
891
|
const sourceVersion = String(loaded.workspace?.dataModelVersion || "");
|
|
892
892
|
const requestedTarget = options.targetModelVersion
|
|
893
893
|
? String(options.targetModelVersion)
|
|
894
|
-
: sourceVersion === "1" ? V1_TARGET_MODEL_VERSION
|
|
894
|
+
: sourceVersion === "1" ? V1_TARGET_MODEL_VERSION
|
|
895
|
+
: sourceVersion === "2" ? "3"
|
|
896
|
+
: sourceVersion === "3" ? "4"
|
|
897
|
+
: sourceVersion === "4" ? "5"
|
|
898
|
+
: ACTIVE_MODEL_VERSION;
|
|
895
899
|
if (sourceVersion === requestedTarget) return emptyPlan(sourceVersion, requestedTarget);
|
|
896
900
|
if (sourceVersion === "1" && requestedTarget === "2") {
|
|
897
901
|
return planV1ToV2Migration(input, options);
|
|
@@ -902,13 +906,16 @@ export async function planModelMigration(input = process.cwd(), options = {}) {
|
|
|
902
906
|
if (sourceVersion === "3" && requestedTarget === "4") {
|
|
903
907
|
return planV3ToV4Migration(loaded, options);
|
|
904
908
|
}
|
|
905
|
-
if (sourceVersion === "4" && requestedTarget ===
|
|
909
|
+
if (sourceVersion === "4" && requestedTarget === "5") {
|
|
906
910
|
return planV4ToV5Migration(loaded, options);
|
|
907
911
|
}
|
|
912
|
+
if (sourceVersion === "5" && requestedTarget === ACTIVE_MODEL_VERSION) {
|
|
913
|
+
return planV5ToV6Migration(loaded);
|
|
914
|
+
}
|
|
908
915
|
if (sourceVersion === "1" && requestedTarget === ACTIVE_MODEL_VERSION) {
|
|
909
916
|
throw new Error(
|
|
910
917
|
"Model v1 workspaces must migrate to model v2 first. "
|
|
911
|
-
+ "Preview and apply `npx filegrc migrate --to-model 2`, then migrate one version at a time through model
|
|
918
|
+
+ "Preview and apply `npx filegrc migrate --to-model 2`, then migrate one version at a time through model v6."
|
|
912
919
|
);
|
|
913
920
|
}
|
|
914
921
|
throw new Error(`Model migration does not support v${sourceVersion} to v${requestedTarget}.`);
|
|
@@ -1702,6 +1709,144 @@ async function planV4ToV5Migration(loaded, options = {}) {
|
|
|
1702
1709
|
};
|
|
1703
1710
|
}
|
|
1704
1711
|
|
|
1712
|
+
async function planV5ToV6Migration(loaded) {
|
|
1713
|
+
if (!loaded.workspace?.id) throw new Error("Model migration requires a valid Workspace record.");
|
|
1714
|
+
const targetModel = loadModel("6");
|
|
1715
|
+
const revisions = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
1716
|
+
const automatic = [];
|
|
1717
|
+
const reviewRequired = [];
|
|
1718
|
+
const unsupported = [];
|
|
1719
|
+
const missing = [];
|
|
1720
|
+
const manualActions = [];
|
|
1721
|
+
const updates = [];
|
|
1722
|
+
const legacyTrainingIds = [];
|
|
1723
|
+
const removedTrainingScheduleFields = [];
|
|
1724
|
+
const obligations = loaded.resources.filter(({ type }) => type === "obligation");
|
|
1725
|
+
|
|
1726
|
+
for (const original of loaded.resources) {
|
|
1727
|
+
if (original.type === "workspace") {
|
|
1728
|
+
updates.push({ ...original, dataModelVersion: "6" });
|
|
1729
|
+
automatic.push(classifiedChange(
|
|
1730
|
+
"automatic",
|
|
1731
|
+
original.id,
|
|
1732
|
+
"dataModelVersion",
|
|
1733
|
+
"Select model v6 and enable separate Training approval and activation."
|
|
1734
|
+
));
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (original.type !== "training") continue;
|
|
1738
|
+
const record = { ...original };
|
|
1739
|
+
if (record.approvedByIds) {
|
|
1740
|
+
record.approverIds = record.approvedByIds;
|
|
1741
|
+
delete record.approvedByIds;
|
|
1742
|
+
automatic.push(classifiedChange(
|
|
1743
|
+
"automatic",
|
|
1744
|
+
record.id,
|
|
1745
|
+
"approverIds",
|
|
1746
|
+
"Use the common governed-content approver field."
|
|
1747
|
+
));
|
|
1748
|
+
}
|
|
1749
|
+
if (record.effectiveContentRevisions) {
|
|
1750
|
+
record.approvedContentRevisions = record.effectiveContentRevisions;
|
|
1751
|
+
delete record.effectiveContentRevisions;
|
|
1752
|
+
automatic.push(classifiedChange(
|
|
1753
|
+
"automatic",
|
|
1754
|
+
record.id,
|
|
1755
|
+
"approvedContentRevisions",
|
|
1756
|
+
"Preserve the exact Training revision previously bound to approval and activation as the approved revision."
|
|
1757
|
+
));
|
|
1758
|
+
}
|
|
1759
|
+
const removedSchedule = {};
|
|
1760
|
+
for (const field of ["assignmentTrigger", "completionWindowDays"]) {
|
|
1761
|
+
if (record[field] === undefined) continue;
|
|
1762
|
+
removedSchedule[field] = record[field];
|
|
1763
|
+
delete record[field];
|
|
1764
|
+
}
|
|
1765
|
+
if (Object.keys(removedSchedule).length) {
|
|
1766
|
+
const obligationIds = obligations.filter((obligation) => (
|
|
1767
|
+
obligation.templateResourceId === record.id
|
|
1768
|
+
|| (obligation.scopeResourceIds || []).includes(record.id)
|
|
1769
|
+
)).map(({ id }) => id);
|
|
1770
|
+
removedTrainingScheduleFields.push({ trainingId: record.id, values: removedSchedule, obligationIds });
|
|
1771
|
+
reviewRequired.push(classifiedChange(
|
|
1772
|
+
"review-required",
|
|
1773
|
+
record.id,
|
|
1774
|
+
"assignmentSchedule",
|
|
1775
|
+
`Training assignment schedules now belong only in Obligations. Confirm the removed ${Object.keys(removedSchedule).join(" and ")} values against ${obligationIds.length ? obligationIds.join(", ") : "a new Step 3 Obligation"}.`
|
|
1776
|
+
));
|
|
1777
|
+
}
|
|
1778
|
+
if (record.status === "draft" && record.effectiveOn) {
|
|
1779
|
+
record.proposedEffectiveOn = record.effectiveOn;
|
|
1780
|
+
delete record.effectiveOn;
|
|
1781
|
+
reviewRequired.push(classifiedChange(
|
|
1782
|
+
"review-required",
|
|
1783
|
+
record.id,
|
|
1784
|
+
"proposedEffectiveOn",
|
|
1785
|
+
"Keep the draft Training date as proposed until the approved revision is activated in Step 3."
|
|
1786
|
+
));
|
|
1787
|
+
}
|
|
1788
|
+
if (["active", "retired"].includes(record.status) && record.approvedContentRevisions) {
|
|
1789
|
+
record.activationBasis = "legacy-v5";
|
|
1790
|
+
legacyTrainingIds.push(record.id);
|
|
1791
|
+
reviewRequired.push(classifiedChange(
|
|
1792
|
+
"review-required",
|
|
1793
|
+
record.id,
|
|
1794
|
+
"activationBasis",
|
|
1795
|
+
"Preserve the combined model v5 Training approval and activation as legacy-v5. The migration does not invent a separate activation actor, date, or revision."
|
|
1796
|
+
));
|
|
1797
|
+
}
|
|
1798
|
+
updates.push(record);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const updatedById = new Map(updates.map((record) => [record.id, record]));
|
|
1802
|
+
const migratedRecords = loaded.resources.map((record) => updatedById.get(record.id) || record);
|
|
1803
|
+
await collectTargetValidationActions(loaded, migratedRecords, targetModel, missing, manualActions);
|
|
1804
|
+
for (const item of [...missing, ...manualActions]) {
|
|
1805
|
+
unsupported.push(classifiedChange(
|
|
1806
|
+
"unsupported",
|
|
1807
|
+
item.resourceId,
|
|
1808
|
+
item.field,
|
|
1809
|
+
item.message || `Resolve ${item.field} before applying the model v6 migration.`
|
|
1810
|
+
));
|
|
1811
|
+
}
|
|
1812
|
+
const ready = unsupported.length === 0;
|
|
1813
|
+
return {
|
|
1814
|
+
schemaVersion: 2,
|
|
1815
|
+
sourceModelVersion: "5",
|
|
1816
|
+
targetModelVersion: "6",
|
|
1817
|
+
ready,
|
|
1818
|
+
missing,
|
|
1819
|
+
conflicts: [],
|
|
1820
|
+
manualActions,
|
|
1821
|
+
classifications: { automatic, reviewRequired, unsupported },
|
|
1822
|
+
notes: reviewRequired,
|
|
1823
|
+
migrationReport: { legacyTrainingIds, removedTrainingScheduleFields },
|
|
1824
|
+
summary: {
|
|
1825
|
+
create: 0,
|
|
1826
|
+
update: updates.length,
|
|
1827
|
+
automatic: automatic.length,
|
|
1828
|
+
reviewRequired: reviewRequired.length,
|
|
1829
|
+
unsupported: unsupported.length
|
|
1830
|
+
},
|
|
1831
|
+
fileDiff: {
|
|
1832
|
+
create: [],
|
|
1833
|
+
update: updates.map((record) => ({
|
|
1834
|
+
type: record.type,
|
|
1835
|
+
id: record.id,
|
|
1836
|
+
before: loaded.resources.find(({ id }) => id === record.id),
|
|
1837
|
+
after: record
|
|
1838
|
+
}))
|
|
1839
|
+
},
|
|
1840
|
+
changes: {
|
|
1841
|
+
create: [],
|
|
1842
|
+
update: updates,
|
|
1843
|
+
expectedRevisions: Object.fromEntries(updates.map(({ id }) => [id, revisions.get(id)])),
|
|
1844
|
+
validateWholeWorkspace: true,
|
|
1845
|
+
targetModelVersion: "6"
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1705
1850
|
function normalizeDocumentScopeDecision(value) {
|
|
1706
1851
|
const candidate = typeof value === "object" && value
|
|
1707
1852
|
? value.workflowScope || value.scope
|
package/src/obligations.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./recurrence.js";
|
|
15
15
|
import { currentCalendarDate, isRfc3339Timestamp } from "./time.js";
|
|
16
16
|
import { loadWorkspace } from "./workspace.js";
|
|
17
|
-
import {
|
|
17
|
+
import { obligationGovernedContent, obligationProgramStatus } from "./program-lifecycle.js";
|
|
18
18
|
import { resolveProgram } from "./program.js";
|
|
19
19
|
|
|
20
20
|
const COMPLETION_DATE_FIELDS = [
|
|
@@ -259,7 +259,7 @@ export async function createObligationEvent(input, options) {
|
|
|
259
259
|
));
|
|
260
260
|
if (!eventType || templates.length === 0) throw new Error(`No active obligations use event type "${eventType}".`);
|
|
261
261
|
if (templates.some((record) => obligationProgramStatus(record, byId, occurredOn, loaded.model) === "proposed")) {
|
|
262
|
-
throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing Policy and required governed
|
|
262
|
+
throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing Policy and required governed-content record active and effective, then implement at least one linked Control before starting this workflow.`);
|
|
263
263
|
}
|
|
264
264
|
if (templates.some((record) => normalizedEventWindow(record.window).precision === "timestamp") && !occurredAt) {
|
|
265
265
|
throw new Error(`Event type "${eventType}" has hour-based deadlines and requires an RFC 3339 occurredAt timestamp.`);
|
|
@@ -997,10 +997,10 @@ function obligationActivationDate(obligation, byId, model) {
|
|
|
997
997
|
.filter((policy) => policy?.type === "policy")
|
|
998
998
|
.map((policy) => policy.effectiveOn)
|
|
999
999
|
.filter(Boolean);
|
|
1000
|
-
const
|
|
1001
|
-
.map((
|
|
1000
|
+
const governedContentDates = obligationGovernedContent(obligation, byId, model)
|
|
1001
|
+
.map((record) => record.effectiveOn)
|
|
1002
1002
|
.filter(Boolean);
|
|
1003
|
-
return [...policyDates, ...
|
|
1003
|
+
return [...policyDates, ...governedContentDates].sort().at(-1) || null;
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
1006
|
function relativeTiming(window, asOf) {
|
|
@@ -242,13 +242,13 @@ Reported events receive an owner, assessment, and documented resolution or escal
|
|
|
242
242
|
|
|
243
243
|
## Business Continuity and Disaster Recovery Policy
|
|
244
244
|
|
|
245
|
-
Each important System records
|
|
245
|
+
Each important System records recovery priorities, dependencies, responsible people, alternate communication and access needs, and a backup or alternate recovery approach suited to its commitments, business impact, data risk, dependencies, and technical capability. Numeric recovery targets are required only when an approved customer commitment, included Availability criterion, or management risk decision calls for them.
|
|
246
246
|
|
|
247
247
|
The Security Incident and Recovery Plan records activation, communication, response, recovery, and return-to-normal responsibilities. Management tests continuity and disaster recovery on the approved schedule, records results and findings, and tracks follow-up work.
|
|
248
248
|
|
|
249
249
|
## Backup and Restoration Policy
|
|
250
250
|
|
|
251
|
-
Important Systems use backups or an approved alternate recovery approach
|
|
251
|
+
Important Systems use backups or an approved alternate recovery approach suited to their recovery needs and any approved recovery targets. Management documents backup or alternate-recovery scope, frequency, retention, encryption and access needs, monitoring, failure response, procedures, and test schedules.
|
|
252
252
|
|
|
253
253
|
Backup or recovery access is limited to authorized people and protected from the failures it is intended to address. Restoration or alternate recovery is validated on the approved schedule and after material change when prior results no longer represent the System. Policy adoption does not assert that every System uses daily backups or a fixed retention period.
|
|
254
254
|
|
package/src/policy-library.js
CHANGED
|
@@ -6,7 +6,7 @@ import { serializeWorkspaceMutation } from "./mutation.js";
|
|
|
6
6
|
import { resolveDataPath } from "./paths.js";
|
|
7
7
|
import { loadWorkspace } from "./workspace.js";
|
|
8
8
|
|
|
9
|
-
export const INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID = "consolidated-information-security-policy-
|
|
9
|
+
export const INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID = "consolidated-information-security-policy-v4";
|
|
10
10
|
export const STRONG_AUTHENTICATION_LIBRARY_PROPOSAL_ID = INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID;
|
|
11
11
|
|
|
12
12
|
const POLICY_ID = "policy-information-security";
|
|
@@ -42,28 +42,33 @@ const DOCUMENT_CONTENT_UPDATES = [
|
|
|
42
42
|
id: "document-data-retention-schedule",
|
|
43
43
|
path: "documents/document-data-retention-schedule.md",
|
|
44
44
|
priorRevision: "45a408e8139bd57f42dda5ca5ae5c8cd4480b4e7bf08834f60058148a3a63475",
|
|
45
|
-
|
|
45
|
+
additionalPriorRevisions: new Set(["d80b99ce53d1012cc169bbbc2afab8d0597bfbe9f30ac0812a8d5bbeb2ed9f90"]),
|
|
46
|
+
currentRevision: "dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7",
|
|
46
47
|
replacements: [
|
|
47
48
|
["FileGRC detects the bracketed prompts as approval blockers. Remove each prompt only after replacing it with a reviewed fact.", "Remove each bracketed prompt only after replacing it with a reviewed fact."],
|
|
48
|
-
["Record the authority, scope, owner, start date, and release decision outside this public template.", "Record the authority, scope, owner, start date, and release decision in controlled legal-hold records."]
|
|
49
|
+
["Record the authority, scope, owner, start date, and release decision outside this public template.", "Record the authority, scope, owner, start date, and release decision in controlled legal-hold records."],
|
|
50
|
+
["| Production backups or alternate recovery copies | [Complete before approval: Systems or Components] | [Complete before approval: owner] | Backup or recovery-copy creation | [Confirm or replace proposed default before approval: 30 days, adjusted to approved System recovery objectives] | [Complete before approval: expiration or disposal action] | [Complete before approval: continuity objective or risk decision] |", "| Production backups or alternate recovery copies | [Complete before approval: Systems or Components] | [Complete before approval: owner] | Backup or recovery-copy creation | [Confirm or replace proposed default before approval: 30 days, adjusted to approved System recovery needs] | [Complete before approval: expiration or disposal action] | [Complete before approval: recovery need, commitment, or risk decision] |"]
|
|
49
51
|
],
|
|
50
|
-
summary: "Keep FileGRC prompt handling in document guidance
|
|
52
|
+
summary: "Keep FileGRC prompt handling in document guidance, make the Retention Schedule standalone, and avoid assuming numeric recovery objectives."
|
|
51
53
|
},
|
|
52
54
|
{
|
|
53
55
|
id: "document-security-incident-recovery-plan",
|
|
54
56
|
path: "documents/document-security-incident-recovery-plan.md",
|
|
55
57
|
priorRevision: "339ff564fa0ec26503c2498e8d3c44529957113cf782aa03ddb5b4e64c8f0404",
|
|
56
|
-
|
|
58
|
+
additionalPriorRevisions: new Set(["51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae"]),
|
|
59
|
+
currentRevision: "558b6fa71eaabf4ea3cfe77eaa24f827ad66a19a059085b2090e3c4a4d79cb50",
|
|
57
60
|
replacements: [
|
|
58
61
|
["Controls, Components, Systems, Obligations, and Evidence hold the actual technical configuration and proof of operation.", "Supporting procedures, system records, and retained evidence document the actual technical configuration and operation."],
|
|
59
62
|
["If an incident raises a legal, privacy, or insurance question, the incident lead gets suitable advice at that time. FileGRC does not require pre-arranged counsel, in-house counsel, or a standing legal retainer.", "If an incident raises a legal, privacy, or insurance question, the incident lead obtains suitable advice at that time. A pre-arranged counsel relationship or standing legal retainer is required only when management determines that the organization's obligations and risk warrant one."],
|
|
60
63
|
["[Complete before activation: Link every important System and record its approved recovery time objective, recovery point objective, maximum tolerable downtime, dependencies, owner, and critical customer commitments in the System record.]", "[Complete before activation: Document every important System's approved recovery time objective, recovery point objective, maximum tolerable downtime, dependencies, owner, and critical customer commitments.]"],
|
|
64
|
+
["[Complete before activation: Document every important System's approved recovery time objective, recovery point objective, maximum tolerable downtime, dependencies, owner, and critical customer commitments.]", "[Complete before activation: Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]"],
|
|
61
65
|
["For each important System, the applicable Control, Component, System, and Obligation records must identify:", "Supporting recovery documentation for each important System must identify:"],
|
|
62
66
|
["- Restore-validation method and governed schedule", "- Restore-validation method and approved schedule"],
|
|
67
|
+
["- Dependencies, fallback paths, and validation steps", "- Dependencies, fallback paths, and validation steps\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
|
|
63
68
|
["[Confirm or replace before activation: The starter proposal for important production data is a daily backup, 30-day retention period, and annual restore validation. Record the approved choice for every important System in its Control, Component, System, Retention Schedule, and Obligation records.]", "[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]"],
|
|
64
69
|
["on the approved governed schedule", "on the approved schedule"]
|
|
65
70
|
],
|
|
66
|
-
summary: "Express incident and recovery requirements as a standalone plan
|
|
71
|
+
summary: "Express incident and recovery requirements as a standalone plan, keep FileGRC record-entry guidance outside it, and make numeric recovery targets conditional."
|
|
67
72
|
},
|
|
68
73
|
{
|
|
69
74
|
id: "document-soc2-management-representation",
|
|
@@ -108,7 +113,8 @@ const PRIOR_STARTER_POLICY_REVISIONS = new Set([
|
|
|
108
113
|
"18f5e7b7e417b494a5cd241751269b3aafeb3ad9129002308c84c1c84419ee31",
|
|
109
114
|
"432ba5797c2d23e02a6b09d07f1a33db29337b4bcb3ee7d68ce8495dc7c7c01c",
|
|
110
115
|
"6775e6907c2a094c5f48090b12af14cb78b20d1e45178cbb5d3bf02cabde59c6",
|
|
111
|
-
"992ef090273ae9bd25e729fe4a7a20817926f345cf7a01f242b877f319c4e342"
|
|
116
|
+
"992ef090273ae9bd25e729fe4a7a20817926f345cf7a01f242b877f319c4e342",
|
|
117
|
+
"355cad2754691f7a11ec10ae6ef0a6abeab6c428c51b49d32dfa3c12a2262e96"
|
|
112
118
|
]);
|
|
113
119
|
|
|
114
120
|
const CONTROL_UPDATES = [
|
|
@@ -325,6 +331,28 @@ const CONTROL_UPDATES = [
|
|
|
325
331
|
},
|
|
326
332
|
summary: "Add conditional service-health and capacity monitoring without prescribing a monitoring product."
|
|
327
333
|
},
|
|
334
|
+
{
|
|
335
|
+
id: "control-backup-restoration",
|
|
336
|
+
prior: [{
|
|
337
|
+
statement: "Each important System has backup scope, frequency, retention, monitoring, and restore validation that meet its approved recovery objectives, or a documented alternate recovery approach when backups are not the chosen safeguard.",
|
|
338
|
+
activity: "Record System recovery objectives and backup or alternate recovery procedures, monitor the chosen safeguards, and validate recovery on the approved schedule."
|
|
339
|
+
}],
|
|
340
|
+
next: {
|
|
341
|
+
statement: "Each important System has risk-based backup or alternate recovery scope, frequency, retention, monitoring, and restore validation suited to its recovery needs and any approved recovery targets.",
|
|
342
|
+
activity: "Record backup or alternate recovery procedures, monitor the chosen safeguards, and validate recovery on the approved schedule."
|
|
343
|
+
},
|
|
344
|
+
summary: "Keep backup and recovery risk-based without requiring numeric recovery targets for Security-only scope."
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "control-continuity-exercise",
|
|
348
|
+
prior: [{
|
|
349
|
+
activity: "Maintain the plan, contacts, recovery objectives, exercises, results, and follow-up work."
|
|
350
|
+
}],
|
|
351
|
+
next: {
|
|
352
|
+
activity: "Maintain the plan, contacts, recovery priorities, exercises, results, and follow-up work."
|
|
353
|
+
},
|
|
354
|
+
summary: "Keep continuity exercises focused on recovery priorities without assuming numeric recovery targets."
|
|
355
|
+
},
|
|
328
356
|
{
|
|
329
357
|
id: "control-vendor-due-diligence",
|
|
330
358
|
prior: [{
|
|
@@ -515,8 +543,11 @@ async function buildPolicyLibraryPlan(loaded) {
|
|
|
515
543
|
const source = await readResourceSource(loaded, TRAINING_CONTENT_PATH);
|
|
516
544
|
const rawSourceRevision = contentRevision(source);
|
|
517
545
|
const sourceRevision = normalizedTrainingRevision(source, loaded.workspace?.organizationName);
|
|
518
|
-
|
|
519
|
-
|
|
546
|
+
const adoptedStatuses = modelSupports(loaded.model, "governed-training-activation")
|
|
547
|
+
? ["approved", "active", "superseded", "retired"]
|
|
548
|
+
: ["active", "retired"];
|
|
549
|
+
if (adoptedStatuses.includes(trainingEntry.record.status)) {
|
|
550
|
+
skipped.push(skippedItem(TRAINING_ID, "adopted", "Approved, active, superseded, and retired Training content is never changed by a starter-library proposal."));
|
|
520
551
|
} else if (sourceRevision === CURRENT_TRAINING_REVISION) {
|
|
521
552
|
skipped.push(skippedItem(TRAINING_ID, "current", "The Security Awareness Training already contains the current starter language."));
|
|
522
553
|
} else if (!PRIOR_TRAINING_REVISIONS.has(sourceRevision)) {
|
|
@@ -563,7 +594,8 @@ async function buildPolicyLibraryPlan(loaded) {
|
|
|
563
594
|
skipped.push(skippedItem(documentUpdate.id, "current", "The governed Document already contains the current standalone starter language."));
|
|
564
595
|
continue;
|
|
565
596
|
}
|
|
566
|
-
if (sourceRevision !== documentUpdate.priorRevision
|
|
597
|
+
if (sourceRevision !== documentUpdate.priorRevision
|
|
598
|
+
&& !documentUpdate.additionalPriorRevisions?.has(sourceRevision)) {
|
|
567
599
|
skipped.push(skippedItem(documentUpdate.id, "customized", "The governed Document differs from the recognized prior starter, so FileGRC will not rewrite it."));
|
|
568
600
|
continue;
|
|
569
601
|
}
|
package/src/program-lifecycle.js
CHANGED
|
@@ -38,6 +38,31 @@ export function governedDocumentIsOperating(document, asOf, model) {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function governedTrainingIsOperating(training, asOf, model) {
|
|
42
|
+
if (training?.type !== "training" || training.status !== "active") return false;
|
|
43
|
+
if (!training.effectiveOn || training.effectiveOn > asOf) return false;
|
|
44
|
+
if (!modelSupports(model, "governed-training-activation")) return true;
|
|
45
|
+
if (training.activationBasis === "legacy-v5") {
|
|
46
|
+
return Boolean(training.approvedOn && training.approvedContentRevisions);
|
|
47
|
+
}
|
|
48
|
+
return Boolean(
|
|
49
|
+
training.activationBasis === "recorded"
|
|
50
|
+
&& training.approvedOn
|
|
51
|
+
&& training.approvedContentRevisions
|
|
52
|
+
&& training.activatedOn
|
|
53
|
+
&& training.activatedOn <= asOf
|
|
54
|
+
&& (training.activatedByIds || []).length
|
|
55
|
+
&& training.activatedContentRevisions
|
|
56
|
+
&& contentRevisionBindingsMatch(training.approvedContentRevisions, training.activatedContentRevisions)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function governedContentIsOperating(record, asOf, model) {
|
|
61
|
+
if (record?.type === "document") return governedDocumentIsOperating(record, asOf, model);
|
|
62
|
+
if (record?.type === "training") return governedTrainingIsOperating(record, asOf, model);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
export function contentRevisionBindingsMatch(left, right) {
|
|
42
67
|
if (!left || !right || Array.isArray(left) || Array.isArray(right)) return false;
|
|
43
68
|
const normalize = (value) => Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
|
|
@@ -66,6 +91,19 @@ export function obligationGovernedDocuments(obligation, byId, model) {
|
|
|
66
91
|
));
|
|
67
92
|
}
|
|
68
93
|
|
|
94
|
+
export function obligationGovernedContent(obligation, byId, model) {
|
|
95
|
+
const documents = obligationGovernedDocuments(obligation, byId, model);
|
|
96
|
+
if (!modelSupports(model, "governed-training-activation")) return documents;
|
|
97
|
+
const directIds = [
|
|
98
|
+
...(obligation.scopeResourceIds || []),
|
|
99
|
+
...(obligation.templateResourceId ? [obligation.templateResourceId] : [])
|
|
100
|
+
];
|
|
101
|
+
const training = [...new Set(directIds)]
|
|
102
|
+
.map((id) => byId.get(id))
|
|
103
|
+
.filter((record) => record?.type === "training" && !["superseded", "retired"].includes(record.status));
|
|
104
|
+
return [...documents, ...training];
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
function indexRequiredDocumentsByControl(byId, model) {
|
|
70
108
|
const modelVersion = String(model?.modelVersion || "");
|
|
71
109
|
const cached = requiredDocumentsByControlCache.get(byId);
|
|
@@ -99,9 +137,9 @@ export function obligationProgramStatus(obligation, byId, asOf, model) {
|
|
|
99
137
|
&& policy.effectiveOn <= asOf;
|
|
100
138
|
});
|
|
101
139
|
if (!policiesReady) return "proposed";
|
|
102
|
-
const
|
|
103
|
-
.every((
|
|
104
|
-
if (!
|
|
140
|
+
const governedContentReady = obligationGovernedContent(obligation, byId, model)
|
|
141
|
+
.every((record) => governedContentIsOperating(record, asOf, model));
|
|
142
|
+
if (!governedContentReady) return "proposed";
|
|
105
143
|
const controlIds = obligation.controlIds || [];
|
|
106
144
|
if (!controlIds.length) return "accepted";
|
|
107
145
|
return controlIds.some((id) => byId.get(id)?.type === "control" && byId.get(id).status === "implemented")
|
package/src/program-path.js
CHANGED
|
@@ -3,7 +3,7 @@ export const RESOURCE_INSTRUCTIONS = {
|
|
|
3
3
|
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.",
|
|
4
4
|
appointment: "Record one person’s dated appointment to a named organizational or program responsibility. Scope it to the workspace, a team, or the records governed by that appointment.",
|
|
5
5
|
team: "Review the starter Security and Risk Oversight team, including its members and chair. Membership and chairs are authoritative on the Team record.",
|
|
6
|
-
system: "Start with the complete bounded System management governs or the auditor will examine. Record its purpose, services, boundary, exclusions, Information Types, owners, and continuity objectives.",
|
|
6
|
+
system: "Start with the complete bounded System management governs or the auditor will examine. Record its purpose, services, boundary, exclusions, Information Types, owners, and any applicable continuity objectives.",
|
|
7
7
|
component: "Add a Component only when it materially delivers a selected System, supports a Control, produces authoritative Evidence, or supports relevant operations. Give every System use a role and rationale.",
|
|
8
8
|
vendor: "Catalog material external provider relationships. Link a supplied Component when it meets the Component inclusion rules, but do not mirror every Vendor into a Component.",
|
|
9
9
|
classification: "Define an ordered information-handling category used by inventory and Evidence Artifacts.",
|
|
@@ -12,7 +12,7 @@ export const RESOURCE_INSTRUCTIONS = {
|
|
|
12
12
|
requirement: "Keep the published criterion as catalog content. Record management applicability and rationale on the selected Program.",
|
|
13
13
|
commitment: "Record supplemental customer promises and service requirements that shape the scope or control design. The Commitment’s systemIds and controlIds are authoritative for what fulfills it.",
|
|
14
14
|
policy: "Tailor each Policy to match what the company is committing to. Clear placeholders, assign an owner and separate approver, then bind approval to the reviewed content. Approval does not prove implementation. Activate the Policy during the Step 3 cutover after reviewing its implementation gaps.",
|
|
15
|
-
document: "Complete required
|
|
15
|
+
document: "Complete required program Documents in Step 2, assign an owner and separate approver, and bind approval to the intended values and exact Markdown. Implement the linked requirements and activate that approved revision in Step 3. Prepare Audit Documents in Step 5.",
|
|
16
16
|
control: "Finish each applicable starter Control with the procedure people follow, its owner, bounded System scope, operating Components, authoritative evidence-source Components, governing Policy and Requirement mappings, and implementation date. Put calendar and event schedules in Obligations.",
|
|
17
17
|
"complementary-control": "Review whether any in-scope Control depends on a customer or carved-out provider action. Record each real dependency, or confirm that the current scope has none.",
|
|
18
18
|
evidence: "Create an Evidence Artifact when a real export, report, screenshot, signed file, or approved external reference exists. Select its authoritative source Component, link the Controls and operating records it supports, retain the fixed artifact or reference, and have another person verify it before audit use.",
|
|
@@ -28,7 +28,7 @@ export const RESOURCE_INSTRUCTIONS = {
|
|
|
28
28
|
"access-grant": "Record each person’s or service account’s access to a Component, including approval, provisioning, changes, and removal.",
|
|
29
29
|
"access-review": "Review access on schedule, record each decision, and assign any access changes that result.",
|
|
30
30
|
"service-account": "Catalog non-human accounts that need separate tracking, including their owner, purpose, System, privilege, and expiry.",
|
|
31
|
-
training: "
|
|
31
|
+
training: "Review and approve the exact Training content in Step 2, then activate the unchanged revision during Step 3 after its linked Controls and assignment Obligations are ready.",
|
|
32
32
|
attestation: "Record each person’s completion or acknowledgement against the exact policy or training revision.",
|
|
33
33
|
"vulnerability-scan": "Record each required scan, including its scope, timing, result, and evidence.",
|
|
34
34
|
vulnerability: "Track confirmed weaknesses that need separate remediation, acceptance, or closure.",
|
|
@@ -98,20 +98,21 @@ export const PROGRAM_PATH = [
|
|
|
98
98
|
{
|
|
99
99
|
id: "policies",
|
|
100
100
|
number: 2,
|
|
101
|
-
title: "Approve Policies
|
|
102
|
-
description: "
|
|
103
|
-
summary: "
|
|
101
|
+
title: "Approve Policies",
|
|
102
|
+
description: "Review governed content and approvals",
|
|
103
|
+
summary: "Review and independently approve Policies, program Documents, and Training content.",
|
|
104
104
|
sections: [
|
|
105
|
-
{ id: "
|
|
106
|
-
{ id: "governed-documents", title: "Governed Plans and Schedules", description: "Complete the intended values in each required program plan or schedule and obtain independent approval of the exact revision before implementation.", steps: ["Review each required plan or schedule and replace every organization placeholder with the intended owner, timing, threshold, scope, or response value.", "Confirm the Document owner, separate approver, linked Controls, and review schedule.", "Record approval and its date against the exact Markdown revision. Leave the Document approved until its linked requirements are implemented in Step 3."], types: [], relatedLinks: [{ type: "document", label: "Governed plans and schedules", href: "#/resources/document?stage=policies&documentScope=program" }], defaultOpen: true }
|
|
105
|
+
{ 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: "#/stage/policies" }], defaultOpen: true }
|
|
107
106
|
],
|
|
108
|
-
resourceTypes: [
|
|
109
|
-
supportingResourceTypes: ["document"],
|
|
107
|
+
resourceTypes: [],
|
|
108
|
+
supportingResourceTypes: ["policy", "document", "training"],
|
|
110
109
|
commands: [
|
|
111
110
|
"filegrc guide policy --json",
|
|
112
111
|
"filegrc guide document --json",
|
|
112
|
+
"filegrc guide training --json",
|
|
113
113
|
"filegrc list policy --json",
|
|
114
114
|
"filegrc list document --json",
|
|
115
|
+
"filegrc list training --json",
|
|
115
116
|
"filegrc get POLICY_ID --mutation"
|
|
116
117
|
]
|
|
117
118
|
},
|
|
@@ -122,15 +123,17 @@ export const PROGRAM_PATH = [
|
|
|
122
123
|
description: "Finish controls and their evidence sources",
|
|
123
124
|
summary: "Describe each Control and connect its evidence source.",
|
|
124
125
|
sections: [
|
|
125
|
-
{ id: "catalog", title: "Control Catalog", description: "Implement
|
|
126
|
+
{ id: "catalog", title: "Control Catalog", description: "Implement approved requirements, configure Obligations, activate approved program content, finish authoritative evidence sources, then activate the Policies at cutover.", 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.", "Review and enable every calendar and event schedule as an Obligation. Enabled work remains dormant until its governing Policy is active.", "Confirm each source Component is active, has an evidence-source role and rationale in the Control's System scope, has current access owners, and includes repeatable retrieval instructions in Record Markdown.", "Implement every requirement linked from an approved program Document or Training record, then activate the unchanged approved revisions with separate activation dates and bindings.", "Use the activation review to inspect planned or partial Controls, inactive governed content, missing Components or sources, missing schedules, and unresolved Exceptions.", "Choose the approved Policies that should take effect, set the real effective date, and confirm the Step 3 cutover."], types: ["control", "complementary-control", "obligation"], defaultOpen: true }
|
|
126
127
|
],
|
|
127
|
-
resourceTypes: ["control", "complementary-control"],
|
|
128
|
+
resourceTypes: ["control", "complementary-control", "obligation"],
|
|
128
129
|
commands: [
|
|
129
130
|
"filegrc guide control --json",
|
|
130
131
|
"filegrc list control --json",
|
|
131
132
|
"filegrc get CONTROL_ID --mutation",
|
|
133
|
+
"filegrc guide obligation --json",
|
|
134
|
+
"filegrc list obligation --json",
|
|
132
135
|
"filegrc review-collection complementary-control --scaffold",
|
|
133
|
-
"filegrc activate-
|
|
136
|
+
"filegrc activate-content --scaffold",
|
|
134
137
|
"filegrc activate-policies --scaffold",
|
|
135
138
|
"filegrc evidence-map --json",
|
|
136
139
|
"filegrc program-readiness --json"
|
|
@@ -144,11 +147,11 @@ export const PROGRAM_PATH = [
|
|
|
144
147
|
summary: "Complete scheduled and event work. Keep dated proof.",
|
|
145
148
|
sections: [
|
|
146
149
|
{ id: "risk", title: "Risk", description: "Maintain the program’s risk assessments and risk register as the service, threats, suppliers, and control needs change.", steps: ["Complete and approve risk assessments on schedule and after material changes.", "Record risks that need treatment, acceptance, or ongoing tracking.", "Add or update controls when the assessment identifies a new or changed response."], types: ["risk-assessment", "risk"], defaultOpen: true },
|
|
147
|
-
{ id: "queue", title: "Work Queue", description: "Complete recurring
|
|
150
|
+
{ id: "queue", title: "Work Queue", description: "Complete recurring occurrences, Policy Event tasks, and assigned follow-up within their required windows.", steps: ["Complete due work within its allowed window and link dated proof.", "Start Policy Events when hiring, departures, incidents, or material changes occur.", "Resolve every other open Action Item from the same queue."], types: ["obligation-event", "data-request"], utility: "obligation-board", defaultOpen: true },
|
|
148
151
|
{ id: "evidence", title: "Evidence Artifacts", description: "Create records only for real exports, reports, screenshots, signed files, or approved external references collected during operation.", steps: ["Create an Evidence Artifact when the artifact exists or an operating record needs fixed supporting proof.", "Select the authoritative source Component, link the Controls and source operating record, and retain the fixed attachment or approved reference.", "Record the collector and Classification, then have another person verify the artifact before audit use."], types: ["evidence"], defaultOpen: true },
|
|
149
152
|
{ id: "governance", title: "Governance", description: "Record formal reviews, oversight meetings, and approved policy or control exceptions.", steps: ["Complete scheduled policy reviews and oversight meetings.", "Record decisions, attendees, follow-up work, and evidence.", "Approve time-bound exceptions before the departure begins."], types: ["policy-review", "meeting", "exception"], defaultOpen: false },
|
|
150
153
|
{ id: "inventories", title: "Assets and Vendors", description: "Maintain the asset inventory and recurring reviews of supplier relationships during operation.", steps: ["Keep ownership, custody, status, and lifecycle current for important assets.", "Perform vendor reviews on schedule and after material supplier changes.", "Link fixed reports and review evidence to the operating records."], types: ["asset", "vendor-review"], defaultOpen: false },
|
|
151
|
-
{ id: "access-training", title: "Access and Training", description: "Inventory service accounts
|
|
154
|
+
{ id: "access-training", title: "Access and Training Completion", description: "Inventory service accounts and retain access decisions, Training assignments, and acknowledgements produced during operation.", steps: ["Catalog service accounts that need separate tracking.", "Preserve access approvals and removals as they occur, then complete periodic access reviews and resolve exceptions.", "Retain each Training assignment and Attestation against the exact active content revision."], types: ["service-account", "access-grant", "access-review", "attestation"], defaultOpen: false },
|
|
152
155
|
{ id: "security", title: "Security Operations", description: "Record vulnerability work, applicable penetration testing, and incident response activity for the period.", steps: ["Retain scan scope, results, vulnerabilities, remediation, and exceptions.", "When the approved applicability review requires penetration testing, record the test and follow-up findings.", "Start the incident workflow when a qualifying event occurs."], types: ["vulnerability-scan", "vulnerability", "penetration-test", "incident"], defaultOpen: false },
|
|
153
156
|
{ id: "resilience", title: "Resilience", description: "Preserve proof that backups, restoration, continuity, and incident exercises work as designed.", steps: ["Record backup restoration tests and their results.", "Run continuity and incident exercises on schedule.", "Assign and close follow-up work from failed objectives or lessons learned."], types: ["backup-test", "exercise"], defaultOpen: false },
|
|
154
157
|
{ id: "issues", title: "Issues and Remediation", description: "Keep observations in the source report and track only confirmed gaps that need a separate remediation lifecycle.", steps: ["Create a Finding only when a confirmed gap needs its own owner, due date, status, or verified closure.", "Use the Finding itself for straightforward remediation; create Action Items only for separate assigned tasks.", "Work Action Items from Work Queue and close the Finding only after remediation is independently verified."], types: ["finding"], defaultOpen: false }
|
|
@@ -156,7 +159,6 @@ export const PROGRAM_PATH = [
|
|
|
156
159
|
resourceTypes: [
|
|
157
160
|
"risk-assessment",
|
|
158
161
|
"risk",
|
|
159
|
-
"obligation",
|
|
160
162
|
"obligation-event",
|
|
161
163
|
"data-request",
|
|
162
164
|
"evidence",
|
|
@@ -168,7 +170,6 @@ export const PROGRAM_PATH = [
|
|
|
168
170
|
"service-account",
|
|
169
171
|
"access-grant",
|
|
170
172
|
"access-review",
|
|
171
|
-
"training",
|
|
172
173
|
"attestation",
|
|
173
174
|
"vulnerability-scan",
|
|
174
175
|
"vulnerability",
|
|
@@ -222,7 +223,7 @@ export const PROGRAM_PATH = [
|
|
|
222
223
|
summary: "Track the CPA engagement, fieldwork, and evidence packet.",
|
|
223
224
|
sections: [
|
|
224
225
|
{ id: "engagement", title: "Engagement", description: "Record the actual CPA engagement, formal scope and dates, requests, and management responses.", steps: ["Create the Audit after the CPA firm is engaged.", "Record the firm-agreed type, scope, systems, criteria, and dates.", "Track incoming requests and approved response material."], types: ["audit", "audit-request"], defaultOpen: true },
|
|
225
|
-
{ id: "fieldwork", title: "Fieldwork", description: "Prepare
|
|
226
|
+
{ id: "fieldwork", title: "Fieldwork", description: "Prepare Audit Documents, reconcile Type 2 populations, review both evidence paths, support testing, and build the indexed packet.", steps: ["Initialize and complete engagement-specific management Documents and populations.", "Approve and activate each Audit Document only when its engagement facts and timing are final.", "Review dated FileGRC operating records and verified Evidence Artifacts for the formal period.", "Reconcile complete populations, link samples, and resolve fieldwork requests and Findings.", "Build the packet from a clean Git revision; it includes FileGRC records, Markdown, Evidence Artifacts, attachments, indexes, history, and checksums."], types: ["audit-population", "control-test"], relatedLinks: [{ type: "document", label: "Audit Documents", href: "#/resources/document?stage=audit&documentScope=audit" }], utility: "audit-packet", defaultOpen: true }
|
|
226
227
|
],
|
|
227
228
|
resourceTypes: ["audit", "audit-request", "audit-population", "control-test"],
|
|
228
229
|
utilities: [
|