filegrc 0.7.0 → 0.7.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 +8 -0
- package/model/v4.json +97 -6
- package/package.json +2 -2
- package/src/audit-preparation.js +465 -48
- package/src/audit-transition.js +5 -0
- package/src/batch-review.js +14 -5
- package/src/cli.js +43 -0
- package/src/evidence-packet.js +202 -36
- package/src/files.js +53 -3
- package/src/git.js +39 -7
- package/src/index.js +6 -0
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +826 -0
- package/src/program-path.js +1 -1
- package/src/program-readiness.js +74 -7
- package/src/setup.js +18 -1
- package/src/soc2.js +227 -0
- package/src/state.js +8 -0
- package/src/validate.js +20 -0
- package/src/web.js +97 -20
- package/src/workflow.js +90 -41
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { applyResourceBatch, contentRevision } from "./files.js";
|
|
4
|
+
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
5
|
+
import { resolveDataPath } from "./paths.js";
|
|
6
|
+
import { loadWorkspace } from "./workspace.js";
|
|
7
|
+
|
|
8
|
+
export const INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID = "consolidated-information-security-policy-v3";
|
|
9
|
+
export const STRONG_AUTHENTICATION_LIBRARY_PROPOSAL_ID = INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID;
|
|
10
|
+
|
|
11
|
+
const POLICY_ID = "policy-information-security";
|
|
12
|
+
const POLICY_CONTENT_PATH = "policies/policy-information-security.md";
|
|
13
|
+
const POLICY_DISPLAY_PATH = `data/${POLICY_CONTENT_PATH}`;
|
|
14
|
+
const TRAINING_ID = "training-security-awareness";
|
|
15
|
+
const TRAINING_CONTENT_PATH = "training/training-security-awareness.md";
|
|
16
|
+
const TRAINING_DISPLAY_PATH = `data/${TRAINING_CONTENT_PATH}`;
|
|
17
|
+
const PRIOR_TRAINING_REVISIONS = new Set([
|
|
18
|
+
"53583484d668697e78d82bc22136d78976191303a06f67d0ad2edb55ba5ee0c6",
|
|
19
|
+
"0c6677c22932eb3734240125bc0f40418a13f31d0a31943f4ecd12754afed7fc",
|
|
20
|
+
"9e020af0de1c137607ba4c3186f04be7a00e0d0006e7fc8c7a109e35faf353cf",
|
|
21
|
+
"bf8ac1a9e22a215ed375b590f3cba767801e2e062069e147290bad2c98fd29aa"
|
|
22
|
+
]);
|
|
23
|
+
const CURRENT_TRAINING_REVISION = "7e55e8df6991d32085ca836c24e3b0015806d04523eca7ed4ee9e51043f05614";
|
|
24
|
+
const PRIOR_PASSWORD_MANAGER_TRAINING = "Use an approved password manager to generate and store a unique password for each account.";
|
|
25
|
+
const CURRENT_CREDENTIAL_STORAGE_TRAINING = "Use a unique password for each account and store it only through the credential-protection method approved for that System.";
|
|
26
|
+
const PRIOR_REMOTE_MFA_TRAINING = "Use approved remote-access methods and multi-factor authentication.";
|
|
27
|
+
const CURRENT_REMOTE_MFA_TRAINING = "Use approved remote-access methods and multi-factor authentication when the Policy, Control, customer commitment, or risk decision requires it.";
|
|
28
|
+
const PRIOR_SECRET_HANDLING_TRAINING = "Keep passwords, recovery codes, private keys, and authentication tokens out of email, chat, tickets, source code, and general-purpose documents.";
|
|
29
|
+
const CURRENT_SECRET_HANDLING_TRAINING = "Keep plaintext passwords, recovery codes, private keys, and authentication tokens out of email, chat, tickets, source code, and general-purpose documents. Approved source-controlled ciphertext must follow the Policy and keep decryption keys separate.";
|
|
30
|
+
const LEGACY_REPOSITORY_SECRET_TRAINING = "Do not put credentials, authentication tokens, or cryptographic keys in this repository or another general-purpose system.";
|
|
31
|
+
const PRIOR_REPOSITORY_SECRET_TRAINING = "Do not put plaintext credentials, authentication tokens, or cryptographic keys in this repository or another general-purpose System. Approved source-controlled ciphertext must meet the Policy's separate-key, access, and rotation conditions.";
|
|
32
|
+
const CURRENT_REPOSITORY_SECRET_TRAINING = "Do not put plaintext credentials, authentication tokens, or cryptographic keys in source repositories or general-purpose Systems. Approved source-controlled ciphertext must meet the Policy's separate-key, access, and rotation conditions.";
|
|
33
|
+
const PRIOR_SOURCE_SECRET_TRAINING = "Keep secrets out of source code, build output, tickets, chat, and logs.";
|
|
34
|
+
const CURRENT_SOURCE_SECRET_TRAINING = "Keep plaintext secrets out of source code, build output, tickets, chat, and logs. Repository access alone must never decrypt approved source-controlled ciphertext.";
|
|
35
|
+
const PRIOR_ENDPOINT_SETTING_TRAINING = "Company-managed devices use the automatic-lock setting recorded in the applicable Endpoint Control, Component, or System.";
|
|
36
|
+
const CURRENT_ENDPOINT_SETTING_TRAINING = "Company-managed devices must use the approved automatic-lock setting.";
|
|
37
|
+
const PRIOR_CONFIGURATION_RECORD_TRAINING = "The applicable Controls, Components, Systems, and governed schedules record the actual tools, settings, approval paths, and evidence.";
|
|
38
|
+
const CURRENT_CONFIGURATION_DOCUMENTATION_TRAINING = "Approved supporting standards, procedures, and schedules document the tools, settings, approval paths, and evidence.";
|
|
39
|
+
const DOCUMENT_CONTENT_UPDATES = [
|
|
40
|
+
{
|
|
41
|
+
id: "document-data-retention-schedule",
|
|
42
|
+
path: "documents/document-data-retention-schedule.md",
|
|
43
|
+
priorRevision: "45a408e8139bd57f42dda5ca5ae5c8cd4480b4e7bf08834f60058148a3a63475",
|
|
44
|
+
currentRevision: "d80b99ce53d1012cc169bbbc2afab8d0597bfbe9f30ac0812a8d5bbeb2ed9f90",
|
|
45
|
+
replacements: [
|
|
46
|
+
["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."],
|
|
47
|
+
["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."]
|
|
48
|
+
],
|
|
49
|
+
summary: "Keep FileGRC prompt handling in document guidance and make the Retention Schedule standalone."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "document-security-incident-recovery-plan",
|
|
53
|
+
path: "documents/document-security-incident-recovery-plan.md",
|
|
54
|
+
priorRevision: "339ff564fa0ec26503c2498e8d3c44529957113cf782aa03ddb5b4e64c8f0404",
|
|
55
|
+
currentRevision: "51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae",
|
|
56
|
+
replacements: [
|
|
57
|
+
["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."],
|
|
58
|
+
["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."],
|
|
59
|
+
["[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.]"],
|
|
60
|
+
["For each important System, the applicable Control, Component, System, and Obligation records must identify:", "Supporting recovery documentation for each important System must identify:"],
|
|
61
|
+
["- Restore-validation method and governed schedule", "- Restore-validation method and approved schedule"],
|
|
62
|
+
["[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.]"],
|
|
63
|
+
["on the approved governed schedule", "on the approved schedule"]
|
|
64
|
+
],
|
|
65
|
+
summary: "Express incident and recovery requirements as a standalone plan and keep FileGRC record-entry guidance outside it."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "document-soc2-management-representation",
|
|
69
|
+
path: "documents/document-soc2-management-representation.md",
|
|
70
|
+
priorRevision: "59e7e5929eeb24fa6bc23e1ed1553206901c33ba249b372ad0f42282ab7a64c7",
|
|
71
|
+
currentRevision: "83de736025d07d8085fba93a212a98e7b3e951aded9894b933eab1e77c3a0d29",
|
|
72
|
+
replacements: [
|
|
73
|
+
["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"]
|
|
74
|
+
],
|
|
75
|
+
summary: "Keep the representation-letter template independent of FileGRC relationship fields."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "document-soc2-period-completeness",
|
|
79
|
+
path: "documents/document-soc2-period-completeness.md",
|
|
80
|
+
priorRevision: "fffe936b701bfab19d1b2037b98bf3a82e2328a124926d714b7ac3a2df952391",
|
|
81
|
+
currentRevision: "139bb7452fb7a0d6bf63564b43de01fe676a9bacfd5a5b2d639e8eaa24e65b60",
|
|
82
|
+
replacements: [
|
|
83
|
+
["Management reconciled every audit-population record linked to this engagement to its authoritative source and included every item relevant to the in-scope system and controls. The generated `population-index.csv` is incorporated into this statement by reference and records each population ID, source system, query, timezone, count, validation, reviewer, conclusion, and fixed export.", "Management reconciled every population used for this engagement to its authoritative source and included every item relevant to the in-scope system and controls. The accompanying `population-index.csv` is incorporated into this statement by reference and records each population reference, source system, query, timezone, count, validation, reviewer, conclusion, and fixed export."],
|
|
84
|
+
["| Population | filegrc population ID | Result or exception |", "| Population | Population reference | Result or exception |"],
|
|
85
|
+
["For a population with zero items, retain the source-Component export or report that produced the zero count.", "For a population with zero items, retain the authoritative-source export or report that produced the zero count."],
|
|
86
|
+
["filegrc and the linked evidence contain the complete populations", "management's records and linked evidence contain the complete populations"]
|
|
87
|
+
],
|
|
88
|
+
summary: "Make management's completeness statement independent of FileGRC record names."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "document-soc2-system-description",
|
|
92
|
+
path: "documents/document-soc2-system-description.md",
|
|
93
|
+
priorRevision: "dc15bc2141be0d63a832eb6f7734bc7381e77582030534bcaf636138782f8e84",
|
|
94
|
+
currentRevision: "929a7cd87ca78a2d9caa349cad0ae23583ad79184f26ba50c1694c62549aab9e",
|
|
95
|
+
replacements: [
|
|
96
|
+
["reconcile it to the filegrc records", "reconcile it to management's authoritative records"],
|
|
97
|
+
["Link the filegrc commitment records.", "Reconcile the summary to supporting commitment records."],
|
|
98
|
+
["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."]
|
|
99
|
+
],
|
|
100
|
+
summary: "Keep the SOC 2 System Description as a generic management deliverable."
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
const PRIOR_STARTER_POLICY_REVISIONS = new Set([
|
|
104
|
+
"b0f9b988a8bd231fe70ce71b6a732970e709b7af7826c1b55f1532f511b6e511",
|
|
105
|
+
"e87961c6a665d73e9d3cce96ab978df87579668e72ea236248680db1f69a5fa1",
|
|
106
|
+
"2b50c75f4f138eebc7154a6b6e0e50b843f016c228632f7c71239ec27c18cbbf",
|
|
107
|
+
"18f5e7b7e417b494a5cd241751269b3aafeb3ad9129002308c84c1c84419ee31",
|
|
108
|
+
"432ba5797c2d23e02a6b09d07f1a33db29337b4bcb3ee7d68ce8495dc7c7c01c",
|
|
109
|
+
"6775e6907c2a094c5f48090b12af14cb78b20d1e45178cbb5d3bf02cabde59c6",
|
|
110
|
+
"992ef090273ae9bd25e729fe4a7a20817926f345cf7a01f242b877f319c4e342"
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const CONTROL_UPDATES = [
|
|
114
|
+
{
|
|
115
|
+
id: "control-policy-management",
|
|
116
|
+
prior: [{
|
|
117
|
+
title: "Policy management",
|
|
118
|
+
statement: "The policy owner reviews governed policies and plans at least annually and after material changes, obtains approval from a separate independent approver, and retains the approved revisions in Git.",
|
|
119
|
+
activity: "Review, approve, communicate, and version policies and plans.",
|
|
120
|
+
requirementIds: ["requirement-soc2-cc5-1", "requirement-soc2-cc5-2", "requirement-soc2-cc5-3"]
|
|
121
|
+
}],
|
|
122
|
+
next: {
|
|
123
|
+
title: "Control and policy management",
|
|
124
|
+
statement: "Management selects and develops manual and technology Controls from approved objectives, commitments, risks, dependencies, and changes, and records each Control's owner, scope, procedure, operation pattern, evidence source, and implementation status. The policy owner reviews Controls, governed policies, and plans at least annually and after material changes, obtains separate approval for governed content, and retains approved revisions in Git.",
|
|
125
|
+
activity: "Review Control design and evidence paths, correct gaps or approve time-bound Exceptions, and review, approve, communicate, and version governed policies and plans.",
|
|
126
|
+
requirementIds: ["requirement-soc2-cc5-1", "requirement-soc2-cc5-2", "requirement-soc2-cc5-3"]
|
|
127
|
+
},
|
|
128
|
+
summary: "Make risk-based manual and technology Control design, evidence paths, and periodic reassessment testable under CC5.1 and CC5.2."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "control-security-communication",
|
|
132
|
+
prior: [{
|
|
133
|
+
title: "Security communication",
|
|
134
|
+
statement: "The organization communicates security responsibilities, approved reporting routes, material changes, and relevant control information to its workforce and outside parties.",
|
|
135
|
+
activity: "Maintain reporting routes and communicate policies, changes, and security information."
|
|
136
|
+
}],
|
|
137
|
+
next: {
|
|
138
|
+
title: "Security information and communication",
|
|
139
|
+
statement: "Management obtains or generates, checks, and uses relevant and reliable information from internal and external sources to operate Controls, and communicates security responsibilities, approved reporting routes, material changes, and relevant Control information to its workforce and outside parties in time for action.",
|
|
140
|
+
activity: "Record material information sources, scope, period, ownership, and known limits; maintain reporting routes; and communicate policies, changes, and security information."
|
|
141
|
+
},
|
|
142
|
+
summary: "Cover the information-quality and timely communication outcomes in CC2.1 through CC2.3."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "control-workforce-expectations",
|
|
146
|
+
prior: [
|
|
147
|
+
{
|
|
148
|
+
statement: "Workers agree to applicable conduct, confidentiality, acceptable-use, and security responsibilities before receiving access and are held accountable for violations.",
|
|
149
|
+
activity: "Complete screening when appropriate, agreements, policy acknowledgement, and corrective action.",
|
|
150
|
+
requirementIds: ["requirement-soc2-cc1-4"],
|
|
151
|
+
operationPattern: "event-driven"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
statement: "Workers are screened before sensitive access when lawful and appropriate to role risk, have the competence needed for assigned duties, agree to applicable conduct, confidentiality, acceptable-use, intellectual-property, and security responsibilities before receiving access, and are held accountable for violations.",
|
|
155
|
+
activity: "Record the role-based screening decision, confirm competence and authority, complete agreements and policy acknowledgement, and take corrective action when needed.",
|
|
156
|
+
requirementIds: ["requirement-soc2-cc1-4"],
|
|
157
|
+
operationPattern: "event-driven"
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
next: {
|
|
161
|
+
statement: "Workers are screened before sensitive access when lawful and appropriate to role risk, have the competence needed for assigned duties, agree to applicable conduct, confidentiality, acceptable-use, intellectual-property, and security responsibilities before receiving access, and are held accountable for violations.",
|
|
162
|
+
activity: "Record the role-based screening decision, confirm competence and authority, complete agreements and policy acknowledgement, and take corrective action when needed.",
|
|
163
|
+
requirementIds: ["requirement-soc2-cc1-4", "requirement-soc2-cc1-5"],
|
|
164
|
+
operationPattern: "mixed"
|
|
165
|
+
},
|
|
166
|
+
summary: "Add lawful, risk-based screening and workforce competence, map individual accountability, and support event and scheduled reviews without mandating a particular background-check product."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "control-risk-assessment",
|
|
170
|
+
prior: [{
|
|
171
|
+
statement: "The organization assesses information security risk at least annually and after material changes, assigns owners and responses, and reviews high and critical risks at least quarterly.",
|
|
172
|
+
activity: "Identify threats and changes, score risk, select treatment, and track review dates."
|
|
173
|
+
}],
|
|
174
|
+
next: {
|
|
175
|
+
statement: "The organization defines security objectives and risk tolerance, assesses information security, fraud, misconduct, dependency, and change risk at least annually and after material changes, assigns owners and responses, and reviews high and critical risks at least quarterly.",
|
|
176
|
+
activity: "Confirm objectives and risk tolerance, identify internal and external threats, fraud and misconduct scenarios, dependencies, and changes, score risk, select treatment, and track review dates."
|
|
177
|
+
},
|
|
178
|
+
summary: "Make objectives, risk tolerance, fraud, misconduct, dependencies, and material change explicit in the risk assessment."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "control-monitoring-remediation",
|
|
182
|
+
prior: [{
|
|
183
|
+
statement: "Management reviews control operation, incidents, test results, exceptions, and findings, then assigns and tracks corrective work through completion.",
|
|
184
|
+
activity: "Review control evidence and track deficiencies, owners, due dates, and verification."
|
|
185
|
+
}],
|
|
186
|
+
next: {
|
|
187
|
+
statement: "Management reviews Control operation, source information, incidents, test results, Exceptions, and findings at least quarterly and after significant failures, then communicates deficiencies and assigns, tracks, and verifies corrective work through completion.",
|
|
188
|
+
activity: "Review control evidence and track deficiencies, owners, due dates, and verification."
|
|
189
|
+
},
|
|
190
|
+
summary: "Set a testable monitoring cadence and require timely deficiency communication and verified correction."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "control-strong-authentication",
|
|
194
|
+
prior: [
|
|
195
|
+
{
|
|
196
|
+
statement: "Important systems use approved authentication settings, protected unique credentials, and multi-factor authentication for administrative, production, source-control, email, identity, and sensitive-data access when supported.",
|
|
197
|
+
activity: "Configure and monitor authentication, credential storage, and privileged roles."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
statement: "Important Systems use approved strong-authentication settings, unique identities, and protected credentials. Multi-factor authentication is required for workforce and administrative access to production, source control, email, identity, and Systems that provide access to Confidential or Restricted data. Customer and external-user authentication requirements follow approved Controls, customer commitments, and risk decisions.",
|
|
201
|
+
activity: "Configure and monitor authentication, credential storage, and privileged roles."
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
statement: "Important Systems use approved strong-authentication settings, unique identities, protected credentials, changed or disabled default credentials, and separate administrative identities or roles when technically supported and appropriate to risk. Multi-factor authentication is required for workforce and administrative access to production, source control, email, identity, and Systems that provide access to Confidential or Restricted data. Customer and external-user authentication requirements follow approved Controls, customer commitments, and risk decisions.",
|
|
205
|
+
activity: "Configure and monitor authentication, credential and recovery-material protection, default credentials, privileged identities or roles, and customer requirements."
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
next: {
|
|
209
|
+
statement: "Important Systems use approved strong-authentication settings, unique identities, protected credentials, changed or disabled default credentials, and separate administrative identities or roles when technically supported and appropriate to risk. Multi-factor authentication is required for workforce and administrative access to production, source control, email, identity, and Systems that provide access to Confidential or Restricted data. Customer and external-user authentication requirements follow approved Controls, customer commitments, and risk decisions. Where required MFA is unavailable, management approves a time-bound Exception with a risk assessment, compensating Controls, an accountable owner, and a review or expiration date.",
|
|
210
|
+
activity: "Configure and monitor authentication, credential and recovery-material protection, default credentials, privileged identities or roles, customer requirements, and approved MFA Exceptions."
|
|
211
|
+
},
|
|
212
|
+
summary: "Keep the scoped MFA commitment and its time-bound exception path, and add default-credential, recovery-material, and privileged-identity safeguards."
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "control-encryption-transmission",
|
|
216
|
+
prior: [{
|
|
217
|
+
statement: "Confidential and Restricted data is encrypted in transit over untrusted networks and at rest in approved systems and on devices, with protected key access.",
|
|
218
|
+
activity: "Configure encryption and approved transfer methods based on classification."
|
|
219
|
+
}],
|
|
220
|
+
next: {
|
|
221
|
+
statement: "Confidential and Restricted data is encrypted in transit over untrusted networks and at rest in approved Systems and on devices, with named key ownership, protected key access, and risk-based key lifecycle controls.",
|
|
222
|
+
activity: "Configure encryption and approved transfer methods based on classification, and control key generation, storage, distribution, rotation, revocation, and recovery as applicable."
|
|
223
|
+
},
|
|
224
|
+
summary: "Add vendor-neutral ownership and lifecycle outcomes for cryptographic keys."
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "control-inventory-configuration",
|
|
228
|
+
prior: [{
|
|
229
|
+
statement: "The organization maintains inventories of important systems, company and approved personal devices, service accounts, vendors, and data stores, with owners, lifecycle state, and secure configuration expectations.",
|
|
230
|
+
activity: "Maintain inventories, baselines, ownership, classification, and approved deviations."
|
|
231
|
+
}],
|
|
232
|
+
next: {
|
|
233
|
+
statement: "The organization maintains inventories of important Systems, Components, company and approved personal devices, software, service accounts, Vendors, and data stores, with owners, lifecycle state, and secure configuration expectations. Unsupported or unneeded important assets are upgraded, isolated, replaced, or retired according to risk.",
|
|
234
|
+
activity: "Maintain inventories, baselines, ownership, classification, lifecycle decisions, secure retirement, and approved deviations."
|
|
235
|
+
},
|
|
236
|
+
summary: "Add Component and software inventory coverage plus risk-based handling of unsupported and retired assets."
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: "control-network-security",
|
|
240
|
+
prior: [{
|
|
241
|
+
statement: "The organization restricts network paths, protects remote access with approved encryption and authentication, and reviews material network access rules at least annually.",
|
|
242
|
+
activity: "Manage boundaries, firewall rules, wireless safeguards, and remote production access."
|
|
243
|
+
}],
|
|
244
|
+
next: {
|
|
245
|
+
statement: "The organization restricts network paths, separates production and nonproduction environments according to data and risk, protects remote access with approved encryption and authentication, and reviews material network access rules at least annually.",
|
|
246
|
+
activity: "Manage boundaries, environment connections, firewall rules, wireless safeguards, and remote production access."
|
|
247
|
+
},
|
|
248
|
+
summary: "Add risk-based production and nonproduction environment separation."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "control-change-management",
|
|
252
|
+
prior: [
|
|
253
|
+
{
|
|
254
|
+
statement: "Material software and infrastructure changes are recorded, tested, approved, deployed through an authorized process, and recoverable. Review is independent when practical; a small team records a risk-appropriate compensating or post-deployment review, or an approved Exception, when independent pre-deployment review is not possible.",
|
|
255
|
+
activity: "Record the reason, author, risk, reviewer or compensating review, test result, deployment, and rollback method.",
|
|
256
|
+
operationPattern: "event-driven"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
statement: "Material software and infrastructure changes are recorded, receive a security design or threat analysis suited to their risk, are tested, approved, deployed through an authorized process, and are recoverable. Review is independent when practical; a small team records a risk-appropriate compensating or post-deployment review, or an approved Exception, when independent pre-deployment review is not possible.",
|
|
260
|
+
activity: "Record the reason, author, risk, security analysis when applicable, reviewer or compensating review, test result, deployment, communication, and rollback method.",
|
|
261
|
+
operationPattern: "event-driven"
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
next: {
|
|
265
|
+
statement: "Source and deployment paths protect against unauthorized changes and malicious software. Material software and infrastructure changes are recorded, receive a security design or threat analysis suited to their risk, are tested, approved, deployed through an authorized process, and are recoverable. Review is independent when practical; a small team records a risk-appropriate compensating or post-deployment review, or an approved Exception, when independent pre-deployment review is not possible.",
|
|
266
|
+
activity: "Record the reason, author, risk, security analysis when applicable, reviewer or compensating review, test result, deployment, communication, and rollback method.",
|
|
267
|
+
operationPattern: "mixed"
|
|
268
|
+
},
|
|
269
|
+
summary: "Add risk-based security analysis and protection against unauthorized source and deployment changes."
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "control-penetration-testing",
|
|
273
|
+
prior: [{
|
|
274
|
+
activity: "Define scope, perform independent testing, review results, and track findings."
|
|
275
|
+
}],
|
|
276
|
+
next: {
|
|
277
|
+
activity: "Review and record applicability and cadence. When testing is required, define its scope and independence, perform the test, review results, and track findings."
|
|
278
|
+
},
|
|
279
|
+
summary: "Keep penetration-testing execution conditional on the approved applicability and cadence decision."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "control-data-retention-disposal",
|
|
283
|
+
prior: [{ operationPattern: "event-driven" }],
|
|
284
|
+
next: { operationPattern: "mixed" },
|
|
285
|
+
summary: "Match the annual retention-schedule review and event-driven disposal activities."
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "control-endpoint-protection",
|
|
289
|
+
prior: [
|
|
290
|
+
{
|
|
291
|
+
activity: "Use continuous platform protection where supported and verify endpoint configuration, update, and compliance state on the risk-based schedule recorded in an Obligation when periodic work is needed.",
|
|
292
|
+
requirementIds: ["requirement-soc2-cc6-6", "requirement-soc2-cc7-1"]
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
activity: "Use continuous platform protection where supported and verify endpoint configuration, update, and compliance state on the risk-based schedule recorded in an Obligation when periodic work is needed.",
|
|
296
|
+
requirementIds: ["requirement-soc2-cc6-6", "requirement-soc2-cc6-8", "requirement-soc2-cc7-1"]
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
next: {
|
|
300
|
+
activity: "Use continuous platform protection where supported and verify endpoint configuration, update, and compliance state on the approved risk-based schedule when periodic work is needed.",
|
|
301
|
+
requirementIds: ["requirement-soc2-cc6-6", "requirement-soc2-cc6-8", "requirement-soc2-cc7-1"]
|
|
302
|
+
},
|
|
303
|
+
summary: "Map endpoint malware protection to CC6.8 and keep implementation scheduling out of the Control description."
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "control-vulnerability-management",
|
|
307
|
+
prior: [{
|
|
308
|
+
activity: "Choose scan coverage and cadence. Review the starter remediation targets of Critical 7 days, High 14 days, Medium 30 days, and Low 90 days, then record the approved targets or time-bound Exceptions."
|
|
309
|
+
}],
|
|
310
|
+
next: {
|
|
311
|
+
activity: "Choose scan coverage and cadence, define approved risk-based remediation targets, and document time-bound Exceptions when a target cannot be met."
|
|
312
|
+
},
|
|
313
|
+
summary: "Keep starter-selection instructions and unapproved remediation targets out of the Control description."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "control-logging-monitoring",
|
|
317
|
+
prior: [{
|
|
318
|
+
statement: "Important systems record and protect security and operational events, retain them according to the approved Data Retention Schedule, and use risk-based alerting, review, and alert-path testing recorded in the applicable Controls and Obligations.",
|
|
319
|
+
activity: "Collect, protect, alert on, test, and review important log output and access."
|
|
320
|
+
}],
|
|
321
|
+
next: {
|
|
322
|
+
statement: "Important Systems record and protect security and operational events, retain them according to the approved Data Retention Schedule, and use risk-based alerting, review, and alert-path testing. Systems with availability commitments, recovery objectives, or material operational dependencies also monitor the health, capacity, failure, and service indicators needed to detect degradation.",
|
|
323
|
+
activity: "Collect, protect, alert on, test, and review important log output, access, and applicable health, capacity, failure, and service indicators."
|
|
324
|
+
},
|
|
325
|
+
summary: "Add conditional service-health and capacity monitoring without prescribing a monitoring product."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "control-vendor-due-diligence",
|
|
329
|
+
prior: [{
|
|
330
|
+
statement: "New Vendors receive risk-based security and privacy review and suitable contractual safeguards before access to Confidential or Restricted data. Vendors that predate Policy adoption receive a documented transition review, deadline, or approved risk acceptance.",
|
|
331
|
+
activity: "Assess service, data, access, assurance, recovery, incidents, and contract terms before access."
|
|
332
|
+
}],
|
|
333
|
+
next: {
|
|
334
|
+
statement: "New Vendors receive risk-based security and privacy review and suitable contractual safeguards before access to Confidential or Restricted data or material reliance by an important service. Applicable contracts address permitted use and confidentiality, security responsibilities, incident notice, access and subprocessor restrictions, continuity, data return or deletion, termination, and assurance rights. Vendors that predate Policy adoption receive a documented transition review, deadline, or approved risk acceptance.",
|
|
335
|
+
activity: "Assess service, data, access, assurance, recovery, incidents, dependencies, supplied Components, and applicable contract safeguards before access or material reliance."
|
|
336
|
+
},
|
|
337
|
+
summary: "Name the contract outcomes considered when a Vendor handles protected data or supports an important service."
|
|
338
|
+
}
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
const OBLIGATION_UPDATES = [
|
|
342
|
+
{
|
|
343
|
+
id: "obligation-quarterly-security-risk-meeting",
|
|
344
|
+
prior: [{ controlIds: ["control-security-governance"] }],
|
|
345
|
+
next: {
|
|
346
|
+
controlIds: [
|
|
347
|
+
"control-security-governance",
|
|
348
|
+
"control-security-communication",
|
|
349
|
+
"control-monitoring-remediation"
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
summary: "Use the quarterly oversight population for governance, information quality, and Control monitoring."
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "obligation-annual-policy-review",
|
|
356
|
+
prior: [{ controlIds: ["control-policy-management"] }],
|
|
357
|
+
next: { controlIds: ["control-policy-management", "control-data-retention-disposal"] },
|
|
358
|
+
summary: "Tie the annual Retention Schedule review to the Data Retention and Disposal Control."
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "obligation-annual-penetration-test",
|
|
362
|
+
prior: [{
|
|
363
|
+
title: "Annual independent penetration test",
|
|
364
|
+
activityType: "penetration-test"
|
|
365
|
+
}],
|
|
366
|
+
next: {
|
|
367
|
+
title: "Annual penetration-testing applicability and cadence review",
|
|
368
|
+
activityType: "risk-assessment"
|
|
369
|
+
},
|
|
370
|
+
summary: "Review penetration-testing applicability annually without asserting that every service requires an annual test."
|
|
371
|
+
}
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const OBLIGATION_ADDITIONS = [
|
|
375
|
+
{
|
|
376
|
+
id: "obligation-annual-control-design-review",
|
|
377
|
+
copyFrom: "obligation-annual-policy-review",
|
|
378
|
+
title: "Annual Control design and evidence-path review",
|
|
379
|
+
activityType: "control-design-review",
|
|
380
|
+
controlIds: ["control-policy-management", "control-monitoring-remediation"]
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "obligation-annual-workforce-competence-review",
|
|
384
|
+
copyFrom: "obligation-annual-security-training",
|
|
385
|
+
title: "Annual workforce security-role competence review",
|
|
386
|
+
activityType: "performance-review",
|
|
387
|
+
controlIds: ["control-workforce-expectations"]
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: "obligation-worker-start-screening",
|
|
391
|
+
copyFrom: "obligation-worker-start-agreements",
|
|
392
|
+
title: "Record the role-based screening and competence decision before sensitive access",
|
|
393
|
+
activityType: "workforce-review",
|
|
394
|
+
controlIds: ["control-workforce-expectations"]
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: "obligation-worker-role-change-training",
|
|
398
|
+
copyFrom: "obligation-worker-role-change-access",
|
|
399
|
+
title: "Assign and complete applicable role-based security training",
|
|
400
|
+
activityType: "role-training",
|
|
401
|
+
window: { precision: "date", startsAfter: 0, dueAfter: 30 },
|
|
402
|
+
controlIds: ["control-workforce-expectations", "control-security-training"]
|
|
403
|
+
}
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
export async function assessPolicyLibraryUpgrades(input = process.cwd()) {
|
|
407
|
+
const loaded = input?.resources && input?.model && input?.entries
|
|
408
|
+
? input
|
|
409
|
+
: await loadWorkspace(input);
|
|
410
|
+
return (await buildPolicyLibraryPlan(loaded)).assessment;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function applyPolicyLibraryUpgrade(input = process.cwd(), proposalId, options = {}) {
|
|
414
|
+
if (options.confirmed !== true) {
|
|
415
|
+
throw new Error("Review the policy-library diff and explicitly confirm the accepted proposal.");
|
|
416
|
+
}
|
|
417
|
+
if (proposalId !== INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID) {
|
|
418
|
+
throw new Error(`Policy-library proposal "${proposalId || ""}" was not found.`);
|
|
419
|
+
}
|
|
420
|
+
return serializeWorkspaceMutation(input, async (root) => {
|
|
421
|
+
const loaded = await loadWorkspace(root);
|
|
422
|
+
const plan = await buildPolicyLibraryPlan(loaded);
|
|
423
|
+
const proposal = plan.assessment.proposals.find(({ id }) => id === proposalId);
|
|
424
|
+
if (!proposal) {
|
|
425
|
+
throw new Error(`Policy-library proposal "${proposalId}" is no longer available. Review the current workspace before trying again.`);
|
|
426
|
+
}
|
|
427
|
+
if (!options.proposalRevision || options.proposalRevision !== proposal.revision) {
|
|
428
|
+
throw new Error("The policy-library proposal changed or its revision was not confirmed. Review the current diff and accept its exact proposal revision.");
|
|
429
|
+
}
|
|
430
|
+
const result = await applyResourceBatch(root, plan.changes);
|
|
431
|
+
return {
|
|
432
|
+
operation: "policy-library-upgrade",
|
|
433
|
+
acceptedProposalId: proposalId,
|
|
434
|
+
changedPaths: proposal.changes.map(({ path }) => path),
|
|
435
|
+
result: {
|
|
436
|
+
createdResourceIds: result.created.map(({ id }) => id),
|
|
437
|
+
updatedResourceIds: result.updated.map(({ id }) => id),
|
|
438
|
+
validation: result.validation ? {
|
|
439
|
+
ok: result.validation.ok,
|
|
440
|
+
diagnostics: result.validation.diagnostics,
|
|
441
|
+
counts: result.validation.counts
|
|
442
|
+
} : null
|
|
443
|
+
},
|
|
444
|
+
assessment: await assessPolicyLibraryUpgrades(root)
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function buildPolicyLibraryPlan(loaded) {
|
|
450
|
+
const byId = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
451
|
+
const proposalChanges = [];
|
|
452
|
+
const skipped = [];
|
|
453
|
+
const creates = [];
|
|
454
|
+
const updates = [];
|
|
455
|
+
const expectedRevisions = {};
|
|
456
|
+
const contentUpdates = {};
|
|
457
|
+
const expectedContentRevisions = {};
|
|
458
|
+
const libraryPolicy = await readFile(new URL("./policy-library/information-security-policy-v2.md", import.meta.url), "utf8");
|
|
459
|
+
const latestPolicyRevision = normalizedPolicyRevision(libraryPolicy);
|
|
460
|
+
|
|
461
|
+
const policyEntry = byId.get(POLICY_ID);
|
|
462
|
+
if (!policyEntry || policyEntry.record.type !== "policy") {
|
|
463
|
+
skipped.push(skippedItem(POLICY_ID, "missing", "The starter Information Security Policy is not present."));
|
|
464
|
+
} else {
|
|
465
|
+
const source = await readPolicySource(loaded);
|
|
466
|
+
const sourceRevision = normalizedPolicyRevision(source, loaded.workspace?.organizationName);
|
|
467
|
+
let recognizedStarter = false;
|
|
468
|
+
if (["approved", "active", "superseded", "retired"].includes(policyEntry.record.status)) {
|
|
469
|
+
skipped.push(skippedItem(POLICY_ID, "adopted", "Approved, active, superseded, and retired Policy content is never changed by a starter-library proposal."));
|
|
470
|
+
} else if (sourceRevision === latestPolicyRevision) {
|
|
471
|
+
recognizedStarter = true;
|
|
472
|
+
skipped.push(skippedItem(POLICY_ID, "current", "The Information Security Policy already contains the current starter language."));
|
|
473
|
+
} else if (!PRIOR_STARTER_POLICY_REVISIONS.has(sourceRevision)) {
|
|
474
|
+
skipped.push(skippedItem(POLICY_ID, "customized", "The Information Security Policy differs from a recognized prior starter, so FileGRC will not rewrite it."));
|
|
475
|
+
} else {
|
|
476
|
+
recognizedStarter = true;
|
|
477
|
+
const nextSource = materializePolicy(libraryPolicy, loaded.workspace?.organizationName, securityContactFromPolicy(source));
|
|
478
|
+
proposalChanges.push({
|
|
479
|
+
resourceType: "policy",
|
|
480
|
+
resourceId: POLICY_ID,
|
|
481
|
+
path: POLICY_DISPLAY_PATH,
|
|
482
|
+
summary: "Replace unchanged prior starter language with the current standalone, risk-based security requirements.",
|
|
483
|
+
diff: fullReplacementDiff(POLICY_DISPLAY_PATH, source, nextSource)
|
|
484
|
+
});
|
|
485
|
+
contentUpdates[POLICY_ID] = { content: nextSource };
|
|
486
|
+
expectedContentRevisions[POLICY_ID] = {
|
|
487
|
+
[POLICY_CONTENT_PATH]: contentRevision(source)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (recognizedStarter) {
|
|
491
|
+
const currentAudience = policyEntry.record.audience || [];
|
|
492
|
+
const nextAudience = ["employees", "contractors"];
|
|
493
|
+
if (sameValue(currentAudience, ["employees", "contractors", "vendors"])) {
|
|
494
|
+
updates.push({ ...policyEntry.record, audience: nextAudience });
|
|
495
|
+
expectedRevisions[POLICY_ID] = policyEntry.revision;
|
|
496
|
+
const displayPath = "data/policies/policy-information-security.json";
|
|
497
|
+
proposalChanges.push({
|
|
498
|
+
resourceType: "policy",
|
|
499
|
+
resourceId: POLICY_ID,
|
|
500
|
+
path: displayPath,
|
|
501
|
+
summary: "Keep workforce acknowledgement separate from Vendor contract and oversight duties.",
|
|
502
|
+
diff: replacementDiff(displayPath, [["audience", currentAudience, nextAudience]])
|
|
503
|
+
});
|
|
504
|
+
} else if (!sameValue(currentAudience, nextAudience)) {
|
|
505
|
+
skipped.push(skippedItem(POLICY_ID, "customized", "The Policy audience differs from a recognized starter, so FileGRC will not rewrite it."));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const trainingEntry = byId.get(TRAINING_ID);
|
|
511
|
+
if (!trainingEntry || trainingEntry.record.type !== "training") {
|
|
512
|
+
skipped.push(skippedItem(TRAINING_ID, "missing", "The starter Security Awareness Training is not present."));
|
|
513
|
+
} else {
|
|
514
|
+
const source = await readResourceSource(loaded, TRAINING_CONTENT_PATH);
|
|
515
|
+
const rawSourceRevision = contentRevision(source);
|
|
516
|
+
const sourceRevision = normalizedTrainingRevision(source, loaded.workspace?.organizationName);
|
|
517
|
+
if (["active", "retired"].includes(trainingEntry.record.status)) {
|
|
518
|
+
skipped.push(skippedItem(TRAINING_ID, "adopted", "Active and retired Training content is never changed by a starter-library proposal."));
|
|
519
|
+
} else if (sourceRevision === CURRENT_TRAINING_REVISION) {
|
|
520
|
+
skipped.push(skippedItem(TRAINING_ID, "current", "The Security Awareness Training already contains the current starter language."));
|
|
521
|
+
} else if (!PRIOR_TRAINING_REVISIONS.has(sourceRevision)) {
|
|
522
|
+
skipped.push(skippedItem(TRAINING_ID, "customized", "The Security Awareness Training differs from the recognized prior starter, so FileGRC will not rewrite it."));
|
|
523
|
+
} else {
|
|
524
|
+
const nextSource = source
|
|
525
|
+
.replace(PRIOR_PASSWORD_MANAGER_TRAINING, CURRENT_CREDENTIAL_STORAGE_TRAINING)
|
|
526
|
+
.replace(PRIOR_REMOTE_MFA_TRAINING, CURRENT_REMOTE_MFA_TRAINING)
|
|
527
|
+
.replace(PRIOR_SECRET_HANDLING_TRAINING, CURRENT_SECRET_HANDLING_TRAINING)
|
|
528
|
+
.replace(LEGACY_REPOSITORY_SECRET_TRAINING, CURRENT_REPOSITORY_SECRET_TRAINING)
|
|
529
|
+
.replace(PRIOR_REPOSITORY_SECRET_TRAINING, CURRENT_REPOSITORY_SECRET_TRAINING)
|
|
530
|
+
.replace(PRIOR_SOURCE_SECRET_TRAINING, CURRENT_SOURCE_SECRET_TRAINING)
|
|
531
|
+
.replace(PRIOR_ENDPOINT_SETTING_TRAINING, CURRENT_ENDPOINT_SETTING_TRAINING)
|
|
532
|
+
.replace(PRIOR_CONFIGURATION_RECORD_TRAINING, CURRENT_CONFIGURATION_DOCUMENTATION_TRAINING);
|
|
533
|
+
proposalChanges.push({
|
|
534
|
+
resourceType: "training",
|
|
535
|
+
resourceId: TRAINING_ID,
|
|
536
|
+
path: TRAINING_DISPLAY_PATH,
|
|
537
|
+
summary: "Keep credential storage vendor-neutral, align MFA with approved requirements, permit controlled source ciphertext, and remove FileGRC record-entry instructions from workforce training.",
|
|
538
|
+
diff: fullReplacementDiff(TRAINING_DISPLAY_PATH, source, nextSource)
|
|
539
|
+
});
|
|
540
|
+
contentUpdates[TRAINING_ID] = { content: nextSource };
|
|
541
|
+
expectedContentRevisions[TRAINING_ID] = {
|
|
542
|
+
[TRAINING_CONTENT_PATH]: rawSourceRevision
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (const documentUpdate of DOCUMENT_CONTENT_UPDATES) {
|
|
548
|
+
const entry = byId.get(documentUpdate.id);
|
|
549
|
+
const displayPath = `data/${documentUpdate.path}`;
|
|
550
|
+
if (!entry || entry.record.type !== "document") {
|
|
551
|
+
skipped.push(skippedItem(documentUpdate.id, "missing", `The starter ${documentUpdate.id} Document is not present.`));
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const source = await readResourceSource(loaded, documentUpdate.path);
|
|
555
|
+
const rawSourceRevision = contentRevision(source);
|
|
556
|
+
const sourceRevision = normalizedDocumentRevision(source, loaded.workspace?.organizationName);
|
|
557
|
+
if (entry.record.status !== "draft") {
|
|
558
|
+
skipped.push(skippedItem(documentUpdate.id, "adopted", "Only draft governed Document content is eligible for a starter-library proposal."));
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (sourceRevision === documentUpdate.currentRevision) {
|
|
562
|
+
skipped.push(skippedItem(documentUpdate.id, "current", "The governed Document already contains the current standalone starter language."));
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (sourceRevision !== documentUpdate.priorRevision) {
|
|
566
|
+
skipped.push(skippedItem(documentUpdate.id, "customized", "The governed Document differs from the recognized prior starter, so FileGRC will not rewrite it."));
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const nextSource = documentUpdate.replacements.reduce(
|
|
570
|
+
(current, [prior, next]) => current.replace(prior, next),
|
|
571
|
+
source
|
|
572
|
+
);
|
|
573
|
+
if (normalizedDocumentRevision(nextSource, loaded.workspace?.organizationName) !== documentUpdate.currentRevision) {
|
|
574
|
+
throw new Error(`The ${documentUpdate.id} starter update does not produce the current governed Document.`);
|
|
575
|
+
}
|
|
576
|
+
proposalChanges.push({
|
|
577
|
+
resourceType: "document",
|
|
578
|
+
resourceId: documentUpdate.id,
|
|
579
|
+
path: displayPath,
|
|
580
|
+
summary: documentUpdate.summary,
|
|
581
|
+
diff: fullReplacementDiff(displayPath, source, nextSource)
|
|
582
|
+
});
|
|
583
|
+
contentUpdates[documentUpdate.id] = { content: nextSource };
|
|
584
|
+
expectedContentRevisions[documentUpdate.id] = {
|
|
585
|
+
[documentUpdate.path]: rawSourceRevision
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (const controlUpdate of CONTROL_UPDATES) {
|
|
590
|
+
const controlEntry = byId.get(controlUpdate.id);
|
|
591
|
+
const displayPath = `data/controls/${controlUpdate.id}.json`;
|
|
592
|
+
if (!controlEntry || controlEntry.record.type !== "control") {
|
|
593
|
+
skipped.push(skippedItem(controlUpdate.id, "missing", `The starter ${controlUpdate.id} Control is not present.`));
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const currentValues = Object.fromEntries(
|
|
597
|
+
Object.keys(controlUpdate.next).map((field) => [field, controlEntry.record[field]])
|
|
598
|
+
);
|
|
599
|
+
if (sameControlValues(currentValues, controlUpdate.next)) {
|
|
600
|
+
skipped.push(skippedItem(controlUpdate.id, "current", "The Control already contains the current starter language."));
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (controlEntry.record.status !== "planned") {
|
|
604
|
+
skipped.push(skippedItem(controlUpdate.id, "operating", "Only planned Controls are eligible for starter-library text updates."));
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const prior = controlUpdate.prior.find((candidate) => sameControlValues(currentValues, candidate));
|
|
608
|
+
if (!prior) {
|
|
609
|
+
skipped.push(skippedItem(controlUpdate.id, "customized", "The Control statement or activity differs from a recognized prior starter, so FileGRC will not rewrite it."));
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
updates.push({ ...controlEntry.record, ...controlUpdate.next });
|
|
613
|
+
expectedRevisions[controlUpdate.id] = controlEntry.revision;
|
|
614
|
+
proposalChanges.push({
|
|
615
|
+
resourceType: "control",
|
|
616
|
+
resourceId: controlUpdate.id,
|
|
617
|
+
path: displayPath,
|
|
618
|
+
summary: controlUpdate.summary,
|
|
619
|
+
diff: replacementDiff(displayPath, Object.keys(controlUpdate.next)
|
|
620
|
+
.filter((field) => !sameValue(prior[field], controlUpdate.next[field]))
|
|
621
|
+
.map((field) => [field, prior[field], controlUpdate.next[field]]))
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
for (const obligationUpdate of OBLIGATION_UPDATES) {
|
|
626
|
+
const entry = byId.get(obligationUpdate.id);
|
|
627
|
+
const displayPath = `data/obligations/${obligationUpdate.id}.json`;
|
|
628
|
+
if (!entry || entry.record.type !== "obligation") {
|
|
629
|
+
skipped.push(skippedItem(obligationUpdate.id, "missing", `The starter ${obligationUpdate.id} Obligation is not present.`));
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const currentValues = Object.fromEntries(
|
|
633
|
+
Object.keys(obligationUpdate.next).map((field) => [field, entry.record[field]])
|
|
634
|
+
);
|
|
635
|
+
if (sameControlValues(currentValues, obligationUpdate.next)) {
|
|
636
|
+
skipped.push(skippedItem(obligationUpdate.id, "current", "The Obligation already contains the current starter settings."));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (entry.record.status !== "proposed") {
|
|
640
|
+
skipped.push(skippedItem(obligationUpdate.id, "operating", "Only proposed Obligations are eligible for starter-library updates."));
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const prior = obligationUpdate.prior.find((candidate) => sameControlValues(currentValues, candidate));
|
|
644
|
+
if (!prior) {
|
|
645
|
+
skipped.push(skippedItem(obligationUpdate.id, "customized", "The Obligation differs from a recognized prior starter, so FileGRC will not rewrite it."));
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
updates.push({ ...entry.record, ...obligationUpdate.next });
|
|
649
|
+
expectedRevisions[obligationUpdate.id] = entry.revision;
|
|
650
|
+
proposalChanges.push({
|
|
651
|
+
resourceType: "obligation",
|
|
652
|
+
resourceId: obligationUpdate.id,
|
|
653
|
+
path: displayPath,
|
|
654
|
+
summary: obligationUpdate.summary,
|
|
655
|
+
diff: replacementDiff(displayPath, Object.keys(obligationUpdate.next)
|
|
656
|
+
.filter((field) => !sameValue(prior[field], obligationUpdate.next[field]))
|
|
657
|
+
.map((field) => [field, prior[field], obligationUpdate.next[field]]))
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const addition of String(loaded.model.modelVersion) === "4" ? OBLIGATION_ADDITIONS : []) {
|
|
662
|
+
if (byId.has(addition.id)) {
|
|
663
|
+
skipped.push(skippedItem(addition.id, "present", "An Obligation with this ID already exists, so FileGRC will not replace it."));
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const source = byId.get(addition.copyFrom)?.record;
|
|
667
|
+
if (!source || source.type !== "obligation" || source.status !== "proposed") {
|
|
668
|
+
skipped.push(skippedItem(addition.id, "missing-basis", "The recognized proposed starter Obligation needed to derive this schedule is not available."));
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const record = {
|
|
672
|
+
id: addition.id,
|
|
673
|
+
type: "obligation",
|
|
674
|
+
title: addition.title,
|
|
675
|
+
status: "proposed",
|
|
676
|
+
activityType: addition.activityType,
|
|
677
|
+
recurrence: structuredClone(source.recurrence),
|
|
678
|
+
ownerIds: [...(source.ownerIds || [])],
|
|
679
|
+
startsOn: source.startsOn,
|
|
680
|
+
...(source.triggerPrompt ? { triggerPrompt: source.triggerPrompt } : {}),
|
|
681
|
+
...(addition.window || source.window ? { window: structuredClone(addition.window || source.window) } : {}),
|
|
682
|
+
controlIds: [...addition.controlIds],
|
|
683
|
+
policyIds: [...(source.policyIds || [])]
|
|
684
|
+
};
|
|
685
|
+
creates.push(record);
|
|
686
|
+
const displayPath = `data/obligations/${addition.id}.json`;
|
|
687
|
+
proposalChanges.push({
|
|
688
|
+
resourceType: "obligation",
|
|
689
|
+
resourceId: addition.id,
|
|
690
|
+
path: displayPath,
|
|
691
|
+
summary: `Add ${addition.title.toLowerCase()} as a reviewable proposed schedule.`,
|
|
692
|
+
diff: fullReplacementDiff(displayPath, "", `${JSON.stringify(record, null, 2)}\n`)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const proposals = proposalChanges.length ? [{
|
|
697
|
+
id: INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID,
|
|
698
|
+
revision: createHash("sha256").update(JSON.stringify(proposalChanges)).digest("hex"),
|
|
699
|
+
title: "Review the SOC 2 Security starter corrections",
|
|
700
|
+
policyIds: policyEntry ? [POLICY_ID] : [],
|
|
701
|
+
status: "review",
|
|
702
|
+
message: "Review the exact diff, then accept its named proposal and revision explicitly. FileGRC changes only recognized, unchanged starter defaults. It does not approve or activate the Policy, mark any Control implemented, or assert that an audit will pass.",
|
|
703
|
+
changes: proposalChanges
|
|
704
|
+
}] : [];
|
|
705
|
+
return {
|
|
706
|
+
assessment: {
|
|
707
|
+
schemaVersion: 1,
|
|
708
|
+
proposals,
|
|
709
|
+
skipped
|
|
710
|
+
},
|
|
711
|
+
changes: {
|
|
712
|
+
create: creates,
|
|
713
|
+
update: updates,
|
|
714
|
+
contentUpdates,
|
|
715
|
+
expectedRevisions,
|
|
716
|
+
expectedContentRevisions
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function readPolicySource(loaded) {
|
|
722
|
+
return readResourceSource(loaded, POLICY_CONTENT_PATH);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function readResourceSource(loaded, path) {
|
|
726
|
+
try {
|
|
727
|
+
return await readFile(resolveDataPath(loaded.root, path), "utf8");
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (error.code === "ENOENT") return "";
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function normalizedPolicyRevision(source, organizationName) {
|
|
735
|
+
let normalized = source.replaceAll("\r\n", "\n");
|
|
736
|
+
if (organizationName) {
|
|
737
|
+
normalized = normalized
|
|
738
|
+
.replace(`for ${organizationName} and its in-scope services`, "for {{company_name}} and its in-scope services")
|
|
739
|
+
.replaceAll(`\n${organizationName} inventories important Systems`, "\n{{company_name}} inventories important Systems")
|
|
740
|
+
.replaceAll(`\n${organizationName} monitors trusted sources`, "\n{{company_name}} monitors trusted sources")
|
|
741
|
+
.replace(`claims about facilities ${organizationName} does not operate`, "claims about facilities {{company_name}} does not operate");
|
|
742
|
+
}
|
|
743
|
+
normalized = normalized.replace(
|
|
744
|
+
/through the primary route at [^\r\n]+ or the usable alternate route documented/,
|
|
745
|
+
"through the primary route at {{security_contact_email}} or the usable alternate route documented"
|
|
746
|
+
);
|
|
747
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function normalizedTrainingRevision(source, organizationName) {
|
|
751
|
+
let normalized = source.replaceAll("\r\n", "\n");
|
|
752
|
+
if (organizationName) {
|
|
753
|
+
normalized = normalized.replace(
|
|
754
|
+
`This training applies to employees and contractors who use ${organizationName} systems or information.`,
|
|
755
|
+
"This training applies to employees and contractors who use {{company_name}} systems or information."
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const securityContact = normalized.match(/Questions and incident reports should be sent to ([^\r\n]+)\./)?.[1];
|
|
759
|
+
if (securityContact) normalized = normalized.replaceAll(securityContact, "{{security_contact_email}}");
|
|
760
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function normalizedDocumentRevision(source, organizationName) {
|
|
764
|
+
let normalized = source.replaceAll("\r\n", "\n");
|
|
765
|
+
if (organizationName) {
|
|
766
|
+
normalized = normalized
|
|
767
|
+
.replace(`how long ${organizationName} keeps`, "how long {{company_name}} keeps")
|
|
768
|
+
.replace(`affects ${organizationName} or an in-scope service`, "affects {{company_name}} or an in-scope service")
|
|
769
|
+
.replace(`# ${organizationName} Management Representation Letter`, "# {{company_name}} Management Representation Letter")
|
|
770
|
+
.replace(`# ${organizationName} Period Completeness Statement`, "# {{company_name}} Period Completeness Statement")
|
|
771
|
+
.replace(`# ${organizationName} SOC 2 System Description`, "# {{company_name}} SOC 2 System Description");
|
|
772
|
+
}
|
|
773
|
+
normalized = normalized.replace(
|
|
774
|
+
/The primary reporting route is [^\r\n]+\./,
|
|
775
|
+
"The primary reporting route is {{security_contact_email}}."
|
|
776
|
+
);
|
|
777
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function materializePolicy(source, organizationName, securityContact) {
|
|
781
|
+
return source
|
|
782
|
+
.replaceAll("{{company_name}}", organizationName || "Organization")
|
|
783
|
+
.replaceAll("{{security_contact_email}}", securityContact || "security@example.com");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function securityContactFromPolicy(source) {
|
|
787
|
+
return source.match(/through the primary route at ([^\r\n]+?) or the usable alternate route documented/)?.[1];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function sameControlValues(left, right) {
|
|
791
|
+
return Object.keys(right).every((field) => sameValue(left[field], right[field]));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function sameValue(left, right) {
|
|
795
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function replacementDiff(path, replacements) {
|
|
799
|
+
return [
|
|
800
|
+
`--- a/${path}`,
|
|
801
|
+
`+++ b/${path}`,
|
|
802
|
+
...replacements.flatMap(([field, before, after]) => [
|
|
803
|
+
`@@ ${field} @@`,
|
|
804
|
+
`-${displayDiffValue(before)}`,
|
|
805
|
+
`+${displayDiffValue(after)}`
|
|
806
|
+
])
|
|
807
|
+
].join("\n");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function displayDiffValue(value) {
|
|
811
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function fullReplacementDiff(path, before, after) {
|
|
815
|
+
return [
|
|
816
|
+
`--- a/${path}`,
|
|
817
|
+
`+++ b/${path}`,
|
|
818
|
+
"@@ replace recognized starter content @@",
|
|
819
|
+
...before.replaceAll("\r\n", "\n").split("\n").map((line) => `-${line}`),
|
|
820
|
+
...after.replaceAll("\r\n", "\n").split("\n").map((line) => `+${line}`)
|
|
821
|
+
].join("\n");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function skippedItem(resourceId, reason, message) {
|
|
825
|
+
return { resourceId, reason, message };
|
|
826
|
+
}
|