create-filegrc 0.2.0 → 0.3.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/LICENSE +1 -1
- package/README.md +31 -4
- package/package.json +3 -3
- package/src/cli.js +106 -16
- package/src/defaults.js +17 -13
- package/src/index.js +265 -32
- package/template/AGENTS.md +21 -35
- package/template/README.md +24 -20
- package/template/WORKSPACE.md +5 -14
- package/template/data/AGENTS.md +8 -8
- package/template/data/action-items/AGENTS.md +2 -2
- package/template/data/audits/AGENTS.md +3 -3
- package/template/data/documents/document-business-continuity-disaster-recovery.json +0 -1
- package/template/data/documents/document-contractor-policy-acknowledgement.json +0 -1
- package/template/data/documents/document-contractor-training-acknowledgement.json +0 -1
- package/template/data/documents/document-data-retention-schedule.json +0 -1
- package/template/data/documents/document-employee-handbook-acknowledgement.json +0 -1
- package/template/data/documents/document-employee-policy-acknowledgement.json +0 -1
- package/template/data/documents/document-employee-training-acknowledgement.json +0 -1
- package/template/data/documents/document-incident-response-plan.json +0 -1
- package/template/data/documents/document-soc2-management-assertion.json +0 -3
- package/template/data/documents/document-soc2-management-representation.json +0 -3
- package/template/data/documents/document-soc2-period-completeness.json +0 -3
- package/template/data/documents/document-soc2-period-completeness.md +2 -2
- package/template/data/documents/document-soc2-system-description.json +0 -3
- package/template/data/documents/document-soc2-system-description.md +3 -3
- package/template/data/evidence/AGENTS.md +3 -3
- package/template/data/obligation-events/AGENTS.md +1 -1
- package/template/data/people/person-policy-owner.json +1 -1
- package/template/data/policies/policy-anti-bribery-corruption.json +0 -1
- package/template/data/policies/policy-clear-desk-screen.json +0 -1
- package/template/data/policies/policy-data-protection-handling.json +0 -1
- package/template/data/policies/policy-employee-handbook.json +0 -1
- package/template/data/policies/policy-information-security.json +0 -1
- package/template/data/policies/policy-mobile-computing-communications.json +0 -1
- package/template/data/systems/system-filegrc-program-repository.md +3 -3
- package/template/data/workspace.json +3 -3
- package/template/docs/filegrc-audit.png +0 -0
- package/template/docs/filegrc-home.png +0 -0
- package/template/docs/filegrc-social-preview.png +0 -0
- package/template/package.json +2 -2
- package/template-parameters.json +46 -2
- package/template/data/people/person-independent-approver.json +0 -9
package/src/index.js
CHANGED
|
@@ -4,32 +4,43 @@ import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { baselineRecordPaths, writeBaselineRecords } from "./defaults.js";
|
|
7
|
+
import { baselineRecordFiles, baselineRecordPaths, writeBaselineRecords } from "./defaults.js";
|
|
8
8
|
|
|
9
9
|
const execute = promisify(execFile);
|
|
10
10
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
11
|
const textExtensions = new Set(["", ".json", ".md", ".txt", ".yml", ".yaml", ".gitignore"]);
|
|
12
|
+
const STARTER_PROFILES = new Set(["foundation", "security"]);
|
|
13
|
+
const SECURITY_TEMPLATE_COLLECTIONS = new Set(["documents", "policies", "training"]);
|
|
12
14
|
|
|
13
|
-
export async function
|
|
15
|
+
export async function createFilegrc(options = {}) {
|
|
14
16
|
const parameterConfig = JSON.parse(await readFile(join(packageRoot, "template-parameters.json"), "utf8"));
|
|
15
17
|
const target = resolve(options.target ?? "filegrc-program");
|
|
18
|
+
const starter = normalizeStarterProfile(options.starter);
|
|
19
|
+
if (options.setup && options.install === false) {
|
|
20
|
+
throw new Error("Combined service setup requires installation. Remove --no-install or run filegrc setup after npm install.");
|
|
21
|
+
}
|
|
22
|
+
validateCombinedSetup(options.setup);
|
|
16
23
|
await assertWritableTarget(target, Boolean(options.force));
|
|
17
|
-
if (options.force) await assertNoTemplateCollisions(target);
|
|
24
|
+
if (options.force) await assertNoTemplateCollisions(target, starter);
|
|
18
25
|
|
|
19
26
|
const prompted = await resolvePromptValues(parameterConfig.parameters, options);
|
|
20
|
-
const
|
|
27
|
+
const engine = await resolveEngine(options);
|
|
28
|
+
const starterText = starterTemplateText(starter, prompted.company_name);
|
|
21
29
|
const values = {
|
|
22
30
|
...prompted,
|
|
23
31
|
effective_date: options.effectiveDate ?? new Date().toISOString().slice(0, 10),
|
|
24
32
|
project_name: normalizePackageName(basename(target)),
|
|
25
|
-
filegrc_version:
|
|
26
|
-
filegrc_version_range:
|
|
33
|
+
filegrc_version: engine.version,
|
|
34
|
+
filegrc_version_range: engine.dependency,
|
|
35
|
+
...starterText
|
|
27
36
|
};
|
|
28
37
|
|
|
29
38
|
await mkdir(target, { recursive: true });
|
|
30
|
-
await copyTemplate(target);
|
|
31
|
-
await renderTemplate(target, parameterConfig, values);
|
|
32
|
-
await writeBaselineRecords(target, values.effective_date);
|
|
39
|
+
await copyTemplate(target, starter);
|
|
40
|
+
await renderTemplate(target, parameterConfig, values, starter);
|
|
41
|
+
await writeBaselineRecords(target, values.effective_date, starter);
|
|
42
|
+
await applyStarterScope(target, starter, values.effective_date);
|
|
43
|
+
const initialResourceCounts = await summarizeResources(target);
|
|
33
44
|
|
|
34
45
|
const installed = options.install !== false;
|
|
35
46
|
if (installed) {
|
|
@@ -39,16 +50,34 @@ export async function createFileGRC(options = {}) {
|
|
|
39
50
|
}
|
|
40
51
|
const joinedExistingWorktree = await isInsideGitWorktree(target);
|
|
41
52
|
if (!joinedExistingWorktree) await run("git", ["init"], target);
|
|
53
|
+
const setup = options.setup ? await runCombinedSetup(target, options.setup) : null;
|
|
54
|
+
const resourceCounts = setup ? await summarizeResources(target) : initialResourceCounts;
|
|
42
55
|
return {
|
|
43
56
|
target,
|
|
44
57
|
values,
|
|
45
|
-
|
|
58
|
+
starter,
|
|
59
|
+
engineVersion: engine.version,
|
|
60
|
+
engineSource: engine.localPath ? "local" : "registry",
|
|
61
|
+
enginePackage: engine.localPath || null,
|
|
62
|
+
dependency: engine.dependency,
|
|
63
|
+
stages: starterStages(starter, initialResourceCounts),
|
|
64
|
+
resourceCounts,
|
|
65
|
+
setup,
|
|
46
66
|
install: installed ? "installed" : "skipped",
|
|
47
67
|
gitMode: joinedExistingWorktree ? "existing-worktree" : "initialized"
|
|
48
68
|
};
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
export
|
|
71
|
+
export function normalizeStarterProfile(value = "security") {
|
|
72
|
+
const profile = String(value || "security").trim().toLowerCase();
|
|
73
|
+
const normalized = profile === "empty" ? "foundation" : profile === "soc2-security" ? "security" : profile;
|
|
74
|
+
if (!STARTER_PROFILES.has(normalized)) {
|
|
75
|
+
throw new Error(`Starter profile must be one of ${[...STARTER_PROFILES].join(", ")}.`);
|
|
76
|
+
}
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function resolveFilegrcVersion(explicitVersion) {
|
|
52
81
|
if (explicitVersion) return cleanVersion(explicitVersion);
|
|
53
82
|
try {
|
|
54
83
|
const { stdout } = await execute("npm", ["view", "filegrc", "version", "--json"], {
|
|
@@ -63,11 +92,35 @@ export async function resolveFileGRCVersion(explicitVersion) {
|
|
|
63
92
|
}
|
|
64
93
|
}
|
|
65
94
|
|
|
95
|
+
async function resolveEngine(options) {
|
|
96
|
+
if (options.filegrcVersion && options.filegrcPackage) {
|
|
97
|
+
throw new Error("Use either filegrcVersion or filegrcPackage, not both.");
|
|
98
|
+
}
|
|
99
|
+
if (!options.filegrcPackage) {
|
|
100
|
+
const version = await resolveFilegrcVersion(options.filegrcVersion);
|
|
101
|
+
return { version, dependency: `^${version}`, localPath: null };
|
|
102
|
+
}
|
|
103
|
+
const localPath = resolve(String(options.filegrcPackage));
|
|
104
|
+
let packageJson;
|
|
105
|
+
try {
|
|
106
|
+
packageJson = JSON.parse(await readFile(join(localPath, "package.json"), "utf8"));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`Could not read a local filegrc package at ${localPath}: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
if (packageJson.name !== "filegrc") {
|
|
111
|
+
throw new Error(`Local package at ${localPath} is named "${packageJson.name || ""}", expected "filegrc".`);
|
|
112
|
+
}
|
|
113
|
+
const version = cleanVersion(packageJson.version);
|
|
114
|
+
return { version, dependency: `file:${localPath.replaceAll("\\", "/")}`, localPath };
|
|
115
|
+
}
|
|
116
|
+
|
|
66
117
|
async function resolvePromptValues(parameters, options) {
|
|
67
118
|
const mapped = {
|
|
68
119
|
company_name: options.companyName,
|
|
69
120
|
policy_owner_name: options.policyOwnerName,
|
|
70
|
-
|
|
121
|
+
policy_owner_email: options.policyOwnerEmail,
|
|
122
|
+
security_contact_email: options.securityContactEmail,
|
|
123
|
+
timezone: options.timezone
|
|
71
124
|
};
|
|
72
125
|
if (options.yes) {
|
|
73
126
|
mapped.company_name ??= "Example Company";
|
|
@@ -77,21 +130,29 @@ async function resolvePromptValues(parameters, options) {
|
|
|
77
130
|
for (const key of Object.keys(mapped)) {
|
|
78
131
|
if (mapped[key] !== undefined && mapped[key] !== null) mapped[key] = String(mapped[key]).trim();
|
|
79
132
|
}
|
|
80
|
-
|
|
81
|
-
if (missing.length) {
|
|
82
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
83
|
-
throw new Error(`Missing required values: ${missing.map(({ key }) => key).join(", ")}`);
|
|
84
|
-
}
|
|
133
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
85
134
|
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
86
135
|
try {
|
|
87
|
-
for (const parameter of
|
|
136
|
+
for (const parameter of parameters.filter(({ required, key }) => required && !mapped[key])) {
|
|
137
|
+
const defaultValue = parameterDefault(parameter, mapped);
|
|
138
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
88
139
|
let value = "";
|
|
89
|
-
while (!value)
|
|
140
|
+
while (!value) {
|
|
141
|
+
value = (await prompt.question(`${parameter.prompt}${suffix}: `)).trim() || defaultValue;
|
|
142
|
+
}
|
|
90
143
|
mapped[parameter.key] = value;
|
|
91
144
|
}
|
|
92
145
|
} finally {
|
|
93
146
|
prompt.close();
|
|
94
147
|
}
|
|
148
|
+
} else {
|
|
149
|
+
for (const parameter of parameters.filter(({ required, key }) => required && !mapped[key])) {
|
|
150
|
+
mapped[parameter.key] = parameterDefault(parameter, mapped);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const missing = parameters.filter(({ key, required }) => required && !mapped[key]);
|
|
154
|
+
if (missing.length) {
|
|
155
|
+
throw new Error(`Missing required values: ${missing.map(({ key }) => key).join(", ")}`);
|
|
95
156
|
}
|
|
96
157
|
for (const key of ["company_name", "policy_owner_name"]) {
|
|
97
158
|
if (/[\u0000-\u001f\u007f]/.test(mapped[key])) {
|
|
@@ -102,12 +163,39 @@ async function resolvePromptValues(parameters, options) {
|
|
|
102
163
|
throw new Error(`${key} cannot contain template token syntax.`);
|
|
103
164
|
}
|
|
104
165
|
}
|
|
105
|
-
|
|
106
|
-
|
|
166
|
+
for (const [key, label] of [
|
|
167
|
+
["policy_owner_email", "Policy owner email"],
|
|
168
|
+
["security_contact_email", "Security contact email"]
|
|
169
|
+
]) {
|
|
170
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mapped[key])) {
|
|
171
|
+
throw new Error(`${label} must be a valid email address.`);
|
|
172
|
+
}
|
|
107
173
|
}
|
|
174
|
+
if (!isTimezone(mapped.timezone)) throw new Error("Program timezone must be a valid IANA time zone.");
|
|
108
175
|
return mapped;
|
|
109
176
|
}
|
|
110
177
|
|
|
178
|
+
function parameterDefault(parameter, mapped) {
|
|
179
|
+
if (parameter.defaultFrom) return mapped[parameter.defaultFrom] || "";
|
|
180
|
+
if (parameter.defaultSource === "local-timezone") return localTimezone();
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function localTimezone() {
|
|
185
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
186
|
+
return isTimezone(timezone) ? timezone : "UTC";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isTimezone(value) {
|
|
190
|
+
if (!value) return false;
|
|
191
|
+
try {
|
|
192
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format();
|
|
193
|
+
return true;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
111
199
|
async function assertWritableTarget(target, force) {
|
|
112
200
|
try {
|
|
113
201
|
if ((await lstat(target)).isSymbolicLink()) {
|
|
@@ -122,9 +210,9 @@ async function assertWritableTarget(target, force) {
|
|
|
122
210
|
}
|
|
123
211
|
}
|
|
124
212
|
|
|
125
|
-
async function assertNoTemplateCollisions(target) {
|
|
213
|
+
async function assertNoTemplateCollisions(target, starter) {
|
|
126
214
|
const collisions = [];
|
|
127
|
-
for (const destinationPath of [...await templateDestinationPaths(), ...baselineRecordPaths()]) {
|
|
215
|
+
for (const destinationPath of [...await templateDestinationPaths(starter), ...baselineRecordPaths(starter)]) {
|
|
128
216
|
await assertNoSymlinkComponents(target, destinationPath);
|
|
129
217
|
try {
|
|
130
218
|
await lstat(join(target, destinationPath));
|
|
@@ -153,20 +241,22 @@ async function assertNoSymlinkComponents(target, relativePath) {
|
|
|
153
241
|
}
|
|
154
242
|
}
|
|
155
243
|
|
|
156
|
-
async function renderTemplate(target, parameterConfig, values) {
|
|
244
|
+
async function renderTemplate(target, parameterConfig, values, starter) {
|
|
157
245
|
const declared = new Set([
|
|
158
246
|
...parameterConfig.parameters.map(({ key }) => key),
|
|
159
247
|
...parameterConfig.generated.map(({ key }) => key)
|
|
160
248
|
]);
|
|
161
|
-
const files = (await templateDestinationPaths()).map((path) => join(target, path));
|
|
249
|
+
const files = (await templateDestinationPaths(starter)).map((path) => join(target, path));
|
|
162
250
|
for (const path of files) {
|
|
163
251
|
if (!textExtensions.has(extname(path)) && basename(path) !== ".gitignore") continue;
|
|
164
252
|
let source = await readFile(path, "utf8");
|
|
165
253
|
const jsonFile = extname(path) === ".json";
|
|
166
|
-
source = source.replace(/\{\{([a-z0-9_]+)\}\}
|
|
254
|
+
source = source.replace(/\{\{([a-z0-9_]+)\}\}(\.)?/g, (match, token, sentencePeriod = "") => {
|
|
167
255
|
if (!declared.has(token)) throw new Error(`Unknown template token "{{${token}}}" in ${path}`);
|
|
168
256
|
if (values[token] === undefined) throw new Error(`No value resolved for template token "{{${token}}}"`);
|
|
169
|
-
|
|
257
|
+
const value = jsonFile ? jsonStringContents(values[token]) : String(values[token]);
|
|
258
|
+
const punctuation = !jsonFile && sentencePeriod && /[.!?]$/u.test(value) ? "" : sentencePeriod;
|
|
259
|
+
return value + punctuation;
|
|
170
260
|
});
|
|
171
261
|
const unresolved = /\{\{([a-z0-9_]+)\}\}/.exec(source);
|
|
172
262
|
if (unresolved) throw new Error(`Unresolved template token "{{${unresolved[1]}}}" in ${path}`);
|
|
@@ -174,20 +264,22 @@ async function renderTemplate(target, parameterConfig, values) {
|
|
|
174
264
|
}
|
|
175
265
|
}
|
|
176
266
|
|
|
177
|
-
async function templateDestinationPaths() {
|
|
267
|
+
async function templateDestinationPaths(starter = "security") {
|
|
178
268
|
const template = join(packageRoot, "template");
|
|
179
269
|
return (await collectFiles(template)).flatMap((source) => {
|
|
180
270
|
const templatePath = relative(template, source);
|
|
271
|
+
if (!includeTemplatePath(templatePath, starter)) return [];
|
|
181
272
|
if (templatePath === "README.md") return [];
|
|
182
273
|
if (templatePath === "WORKSPACE.md") return ["README.md"];
|
|
183
274
|
return [templatePath === "gitignore" ? ".gitignore" : templatePath];
|
|
184
275
|
});
|
|
185
276
|
}
|
|
186
277
|
|
|
187
|
-
async function copyTemplate(target) {
|
|
278
|
+
async function copyTemplate(target, starter = "security") {
|
|
188
279
|
const template = join(packageRoot, "template");
|
|
189
280
|
for (const source of await collectFiles(template)) {
|
|
190
281
|
const templatePath = relative(template, source);
|
|
282
|
+
if (!includeTemplatePath(templatePath, starter)) continue;
|
|
191
283
|
if (templatePath === "README.md") continue;
|
|
192
284
|
const destinationPath = templatePath === "WORKSPACE.md"
|
|
193
285
|
? "README.md"
|
|
@@ -203,6 +295,13 @@ async function copyTemplate(target) {
|
|
|
203
295
|
}
|
|
204
296
|
}
|
|
205
297
|
|
|
298
|
+
function includeTemplatePath(templatePath, starter) {
|
|
299
|
+
if (starter === "security") return true;
|
|
300
|
+
const segments = templatePath.split(/[\\/]/);
|
|
301
|
+
if (segments[0] !== "data" || !SECURITY_TEMPLATE_COLLECTIONS.has(segments[1])) return true;
|
|
302
|
+
return segments.at(-1) === "AGENTS.md";
|
|
303
|
+
}
|
|
304
|
+
|
|
206
305
|
async function collectFiles(directory) {
|
|
207
306
|
const result = [];
|
|
208
307
|
for (const item of await readdir(directory, { withFileTypes: true })) {
|
|
@@ -213,16 +312,150 @@ async function collectFiles(directory) {
|
|
|
213
312
|
return result;
|
|
214
313
|
}
|
|
215
314
|
|
|
315
|
+
async function summarizeResources(target) {
|
|
316
|
+
const counts = {};
|
|
317
|
+
let total = 0;
|
|
318
|
+
for (const path of await collectFiles(join(target, "data"))) {
|
|
319
|
+
if (extname(path) !== ".json") continue;
|
|
320
|
+
const record = JSON.parse(await readFile(path, "utf8"));
|
|
321
|
+
if (!record?.id || !record?.type || !record?.schemaVersion) continue;
|
|
322
|
+
total += 1;
|
|
323
|
+
counts[record.type] = (counts[record.type] || 0) + 1;
|
|
324
|
+
}
|
|
325
|
+
return { total, byType: counts };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function applyStarterScope(target, starter, effectiveDate) {
|
|
329
|
+
if (starter !== "security") return;
|
|
330
|
+
const records = baselineRecordFiles(effectiveDate, starter).map(({ record }) => record);
|
|
331
|
+
const workspacePath = join(target, "data", "workspace.json");
|
|
332
|
+
const workspace = JSON.parse(await readFile(workspacePath, "utf8"));
|
|
333
|
+
const ids = (type) => records.filter((record) => record.type === type).map((record) => record.id);
|
|
334
|
+
await writeFile(workspacePath, `${JSON.stringify({
|
|
335
|
+
...workspace,
|
|
336
|
+
frameworkIds: ids("framework"),
|
|
337
|
+
requirementIds: ids("requirement"),
|
|
338
|
+
controlIds: ids("control")
|
|
339
|
+
}, null, 2)}\n`, "utf8");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function starterStages(starter, counts) {
|
|
343
|
+
const foundationTypes = new Set(["workspace", "renderer-settings", "person", "team", "system"]);
|
|
344
|
+
const foundation = Object.entries(counts.byType)
|
|
345
|
+
.filter(([type]) => foundationTypes.has(type))
|
|
346
|
+
.reduce((total, [, count]) => total + count, 0);
|
|
347
|
+
return [
|
|
348
|
+
{ id: "foundation", status: "created", records: foundation },
|
|
349
|
+
{
|
|
350
|
+
id: "soc2-security",
|
|
351
|
+
status: starter === "security" ? "created" : "skipped",
|
|
352
|
+
records: starter === "security" ? counts.total - foundation : 0
|
|
353
|
+
}
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function starterTemplateText(starter, companyName) {
|
|
358
|
+
if (starter === "foundation") {
|
|
359
|
+
return {
|
|
360
|
+
program_title: `${companyName} GRC Program`,
|
|
361
|
+
program_description: "Governance, risk, and compliance workspace without a preselected framework.",
|
|
362
|
+
program_summary: `This private workspace holds ${companyName}'s governance, risk, compliance, and audit-evidence records. JSON under \`data/\` stores structured records, Markdown stores long-form work, and Git records reviewed changes.`,
|
|
363
|
+
agent_title: "filegrc Workspace Instructions",
|
|
364
|
+
agent_purpose: `This repository is ${companyName}’s foundation filegrc workspace. Engineers and agents maintain the source records under \`data/\`. The \`filegrc\` package validates, searches, edits, and renders those files. No framework or assurance program has been selected yet.`,
|
|
365
|
+
starter_baseline: `## Foundation baseline
|
|
366
|
+
|
|
367
|
+
The generated workspace starts with five structural records:
|
|
368
|
+
|
|
369
|
+
- Workspace and renderer settings
|
|
370
|
+
- The initial active owner
|
|
371
|
+
- An inactive security and risk oversight team that still needs an independent chair
|
|
372
|
+
- The filegrc Git repository as a governance system of record
|
|
373
|
+
- A default 5x5 risk method and Public, Internal, Confidential, and Restricted data classifications
|
|
374
|
+
|
|
375
|
+
This profile does not include framework requirements, policies, governed documents, training, controls, obligations, or audit-management templates. Add and review those records for the selected framework before treating Program Readiness or Audit Readiness as meaningful. Do not infer that an absent control, policy, or schedule is unnecessary.`,
|
|
376
|
+
audit_preparation_guidance: "The foundation profile does not include the local SOC 2 management-document templates used by `prepare-audit`. Add reviewed templates and program scope before initializing audit work. Audit preparation must not invent missing policy, control, or evidence facts.",
|
|
377
|
+
starter_setup: `## Start the program
|
|
378
|
+
|
|
379
|
+
This foundation profile contains the workspace, initial owner, oversight team, renderer settings, and filegrc system of record. It does not select a framework or create proposed policies, controls, obligations, or evidence.
|
|
380
|
+
|
|
381
|
+
1. Run \`npx filegrc setup\` for guided service and goal setup, or use browser onboarding.
|
|
382
|
+
2. Use \`npx filegrc guide --json\` before creating framework requirements, policies, controls, obligations, and evidence sources.
|
|
383
|
+
3. Run \`npx filegrc validate\`, review the Git diff, and commit each reviewed program layer.`
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
program_title: `${companyName} SOC 2 Program`,
|
|
388
|
+
program_description: "SOC 2 Security program based on the AICPA Trust Services Criteria.",
|
|
389
|
+
program_summary: `This private workspace holds ${companyName}'s SOC 2 program records and audit evidence. JSON under \`data/\` stores structured records, Markdown stores long-form work, and Git records reviewed changes.`,
|
|
390
|
+
agent_title: "filegrc SOC 2 Workspace Instructions",
|
|
391
|
+
agent_purpose: `This repository is ${companyName}’s filegrc workspace for its SOC 2 program. Engineers and agents maintain the source records under \`data/\`. The \`filegrc\` package validates, searches, edits, and renders those files.`,
|
|
392
|
+
starter_baseline: `## Starter baseline
|
|
393
|
+
|
|
394
|
+
The generated workspace starts with the SOC 2 Security category:
|
|
395
|
+
|
|
396
|
+
- Active framework records for the 2017 Trust Services Criteria with revised points of focus (2022) and the 2018 SOC 2 Description Criteria with revised implementation guidance (2022)
|
|
397
|
+
- The 33 Common Criteria reference IDs from CC1.1 through CC9.2, without the licensed criteria text
|
|
398
|
+
- The nine Description Criteria reference IDs from DC1 through DC9, without the licensed criteria text
|
|
399
|
+
- Planned controls mapped to those references and the included policies
|
|
400
|
+
- A security and risk oversight team chaired by an independent reviewer who may be internal or external
|
|
401
|
+
- Recurring obligations for the reviews, scans, tests, training, and meetings required by the included policies
|
|
402
|
+
- A default 5x5 risk method and Public, Internal, Confidential, and Restricted data classifications
|
|
403
|
+
|
|
404
|
+
Treat every planned control as a proposal until its owner, actual procedure in Record Markdown, system scope, cadence, authoritative evidence sources, implementation date, and mappings match actual practice. For a control linked to filegrc obligations, every non-retired Work Queue schedule must be enabled and its governing policies effective. Marking the control implemented starts eligible schedules. Do not mark a control implemented because a policy describes it. Add Availability, Processing Integrity, Confidentiality, or Privacy criteria only when they are in scope.
|
|
405
|
+
|
|
406
|
+
The recurring obligations mirror the fixed cadences in the starter policies. They remain proposals until every governing policy is active and effective and, when they name controls, at least one linked control is implemented. Update the policy, control, and obligation together when an approved cadence changes. Create separate completion records, such as meetings, reviews, scans, tests, exercises, and attestations, for each period.`,
|
|
407
|
+
audit_preparation_guidance: "Preparation creates a separate system description, management assertion, and management representation document for the engagement from the local starter templates. Type 2 preparation also creates a period completeness statement and one `audit-population` record for each standard population. It is safe to run again and does not approve documents, mark controls implemented, or create evidence. Do not reuse one completed management document across engagements.",
|
|
408
|
+
starter_setup: `## Finish initial setup
|
|
409
|
+
|
|
410
|
+
The starter policies, controls, and obligations are proposals. They do not state that ${companyName} operates the described controls.
|
|
411
|
+
|
|
412
|
+
1. Run \`npx filegrc setup\` for guided service and goal setup, or use browser onboarding. Then finish Step 1 by adding the real reviewers and operators, finishing the oversight team, and confirming applicable criteria, commitments, material vendors, and in-scope systems.
|
|
413
|
+
2. Review the starter policies, appoint a reviewer who is separate from the policy owner, and activate only the policies that match current practice. The reviewer will usually be another person in the organization, but may be external.
|
|
414
|
+
3. Review the starter control set, implement each applicable control with its actual procedure, scope, cadence, evidence sources, and implementation date, and confirm any linked Work Queue schedules are enabled. Marking a control implemented starts eligible schedules. Then record any complementary customer or subservice controls.
|
|
415
|
+
4. Preview External Evidence drafts with \`npx filegrc evidence-test-drafts --preview --json\`. Create them only after confirming applicable controls and authoritative source systems.
|
|
416
|
+
5. Run \`npx filegrc program-readiness --require-ready\`, record the management candidate period start when reliable evidence collection begins, maintain risk assessments and risks, update controls when needed, use Work Queue for scheduled work, and trigger Policy Events when changes create required actions.
|
|
417
|
+
6. Engage a CPA firm, record the separate firm-agreed period in an audit record, review filegrc Evidence and External Evidence, and prepare fieldwork.`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function validateCombinedSetup(setup) {
|
|
422
|
+
if (!setup) return;
|
|
423
|
+
if (Array.isArray(setup) || typeof setup !== "object") throw new Error("setup must be a JSON object.");
|
|
424
|
+
const required = ["serviceName", "boundary", "criticality", "dataClassification", "internetExposed"];
|
|
425
|
+
const missing = required.filter((name) => setup[name] === undefined || setup[name] === "");
|
|
426
|
+
if (missing.length) throw new Error(`Combined setup is missing: ${missing.join(", ")}.`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function runCombinedSetup(target, input) {
|
|
430
|
+
const setup = { programGoal: "none", ownerId: "person-policy-owner", ...input };
|
|
431
|
+
const args = [
|
|
432
|
+
join(target, "node_modules", "filegrc", "bin", "filegrc.js"),
|
|
433
|
+
"setup",
|
|
434
|
+
"--service-name", String(setup.serviceName),
|
|
435
|
+
"--boundary", String(setup.boundary),
|
|
436
|
+
"--owner", String(setup.ownerId),
|
|
437
|
+
"--criticality", String(setup.criticality),
|
|
438
|
+
"--classification", String(setup.dataClassification),
|
|
439
|
+
"--internet-exposed", String(setup.internetExposed),
|
|
440
|
+
"--program-goal", String(setup.programGoal),
|
|
441
|
+
"--summary",
|
|
442
|
+
"--json"
|
|
443
|
+
];
|
|
444
|
+
if (setup.draft === true) args.push("--draft");
|
|
445
|
+
const result = await run(process.execPath, args, target);
|
|
446
|
+
return JSON.parse(result.stdout);
|
|
447
|
+
}
|
|
448
|
+
|
|
216
449
|
async function writeMinimalLockfile(target, name, versionRange) {
|
|
217
450
|
const lock = {
|
|
218
451
|
name,
|
|
219
|
-
version: "0.
|
|
452
|
+
version: "0.3.1",
|
|
220
453
|
lockfileVersion: 3,
|
|
221
454
|
requires: true,
|
|
222
455
|
packages: {
|
|
223
456
|
"": {
|
|
224
457
|
name,
|
|
225
|
-
version: "0.
|
|
458
|
+
version: "0.3.1",
|
|
226
459
|
dependencies: { filegrc: versionRange }
|
|
227
460
|
}
|
|
228
461
|
}
|
|
@@ -251,7 +484,7 @@ async function run(command, args, cwd) {
|
|
|
251
484
|
function cleanVersion(value) {
|
|
252
485
|
const version = String(value ?? "").trim().replace(/^v/, "");
|
|
253
486
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
254
|
-
throw new Error(`Could not resolve a valid
|
|
487
|
+
throw new Error(`Could not resolve a valid filegrc version from "${value}".`);
|
|
255
488
|
}
|
|
256
489
|
return version;
|
|
257
490
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# {{agent_title}}
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
{{agent_purpose}}
|
|
6
6
|
|
|
7
7
|
Using this repository does not establish compliance by itself. Records must match actual practice and evidence must prove that controls operated during the audit period.
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ npx filegrc guide --json
|
|
|
15
15
|
npx filegrc program-path --json
|
|
16
16
|
npx filegrc guide risk-assessment --json
|
|
17
17
|
npx filegrc list person --json
|
|
18
|
-
npx filegrc program-readiness --json
|
|
18
|
+
npx filegrc program-readiness --summary --json
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
`program-path` gives agents the same six-step order, exact page Instructions, Use, Policy Basis, commands, current state, and next actions shown in the renderer. The general guide lists every supported action and record type. A type guide repeats that page guidance and adds timing, required and conditional fields, current relationship candidates, JSON location, and Markdown slots.
|
|
@@ -46,7 +46,7 @@ Read `data/AGENTS.md` before changing records. More specific instructions inside
|
|
|
46
46
|
- Let the local app generate IDs from each record’s name or title. When editing JSON directly, keep IDs globally unique, human-readable, and lowercase kebab-case.
|
|
47
47
|
- Use ISO 8601 dates and RFC 3339 timestamps.
|
|
48
48
|
- Store relationships as resource IDs.
|
|
49
|
-
- Put policies, plans, charters, procedures, meeting minutes, training, assertions, narratives, templates, and audit responses in Markdown beside their JSON records.
|
|
49
|
+
- Put policies, plans, charters, procedures, meeting minutes, training, assertions, narratives, templates, and audit responses in Markdown beside their JSON records. filegrc derives the Markdown name, so records do not contain file paths.
|
|
50
50
|
- Put signed forms, screenshots, third-party reports, and immutable exports behind evidence records. These files may be PDF, image, CSV, or another fixed format.
|
|
51
51
|
- Never fetch an external evidence reference automatically.
|
|
52
52
|
- Do not store secrets, credentials, session data, or personal data that may need to be erased from Git history.
|
|
@@ -95,25 +95,11 @@ Headless agents get the same protection by exporting an edit payload with `fileg
|
|
|
95
95
|
|
|
96
96
|
`data/renderer.json` stores committed renderer preferences. New workspaces set `showOnboarding` to `true`. Completing or skipping onboarding sets it to `false`; the app does not commit that change.
|
|
97
97
|
|
|
98
|
-
Onboarding explains the file and Git workflow, the program path, policy obligations, and Policy Events before covering report types and the final audit stage. It then collects the initial service boundary, owner, business criticality, highest data classification, internet exposure, and optional program goal. It creates or updates
|
|
98
|
+
Onboarding explains the file and Git workflow, the program path, policy obligations, and Policy Events before covering report types and the final audit stage. It then collects the initial service boundary, owner, business criticality, highest data classification, internet exposure, and optional program goal. It creates or updates one `system` record and stores that selected system and the management goal on `workspace`. It does not select framework records, link controls to the service, or create evidence. Selecting Type 1 or Type 2 does not create an audit engagement. Completing onboarding opens the Step 1 overview so the user can add the real reviewers and operators, finish the oversight team, and confirm the criteria, commitments, vendors, and systems before approving policies.
|
|
99
99
|
|
|
100
100
|
The renderer is optional. Agents may set `showOnboarding` to `false` and maintain all records headlessly. Restart onboarding from Repository when useful. Read-only builds never run it.
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
The generated workspace starts with the SOC 2 Security category:
|
|
105
|
-
|
|
106
|
-
- Active framework records for the 2017 Trust Services Criteria with revised points of focus (2022) and the 2018 SOC 2 Description Criteria with revised implementation guidance (2022)
|
|
107
|
-
- The 33 Common Criteria reference IDs from CC1.1 through CC9.2, without the licensed criteria text
|
|
108
|
-
- The nine Description Criteria reference IDs from DC1 through DC9, without the licensed criteria text
|
|
109
|
-
- Planned controls mapped to those references and the included policies
|
|
110
|
-
- A security and risk oversight team chaired by an independent reviewer who may be internal or external
|
|
111
|
-
- Recurring obligations for the reviews, scans, tests, training, and meetings required by the included policies
|
|
112
|
-
- A default 5x5 risk method and Public, Internal, Confidential, and Restricted data classifications
|
|
113
|
-
|
|
114
|
-
Treat every planned control as a proposal until its owner, actual procedure in Record Markdown, system scope, cadence, authoritative evidence sources, implementation date, and mappings match actual practice. For a control linked to FileGRC obligations, every non-retired Work Queue schedule must be enabled and its governing policies effective. Marking the control implemented starts eligible schedules. Do not mark a control implemented because a policy describes it. Add Availability, Processing Integrity, Confidentiality, or Privacy criteria only when they are in scope.
|
|
115
|
-
|
|
116
|
-
The recurring obligations mirror the fixed cadences in the starter policies. They remain proposals until every governing policy is active and effective and, when they name controls, at least one linked control is implemented. Update the policy, control, and obligation together when an approved cadence changes. Create separate completion records, such as meetings, reviews, scans, tests, exercises, and attestations, for each period.
|
|
102
|
+
{{starter_baseline}}
|
|
117
103
|
|
|
118
104
|
## Work Queue and Policy Events
|
|
119
105
|
|
|
@@ -141,7 +127,7 @@ npx filegrc trigger person-started --occurred-on 2026-07-25 --subject person-new
|
|
|
141
127
|
npx filegrc trigger person-ended --occurred-at 2026-07-25T16:30:00-05:00 --subject person-departing-worker --json
|
|
142
128
|
```
|
|
143
129
|
|
|
144
|
-
Run `npx filegrc obligations` first to preview every task, owner, deadline, and requested proof for each available Policy Event. The trigger command creates one `obligation-event` and adds its full set of Action Items to the Work Queue in a single validated write. Its success output names the event, task count, task IDs, and deadlines. Hour-based deadlines require an RFC 3339 event timestamp so an immediate or 24-hour cutoff is exact. Day-based deadlines use the event’s calendar date. Link the requested completion resources and evidence to each action item, then mark the actions done and the event complete. Every generated action has a cutoff.
|
|
130
|
+
Run `npx filegrc obligations` first to preview every task, owner, deadline, and requested proof for each available Policy Event. The trigger command creates one `obligation-event` and adds its full set of Action Items to the Work Queue in a single validated write. Its success output names the event, task count, task IDs, and deadlines. Hour-based deadlines require an RFC 3339 event timestamp so an immediate or 24-hour cutoff is exact. Day-based deadlines use the event’s calendar date. Link the requested completion resources and evidence to each action item, then mark the actions done and the event complete. Every generated action has a cutoff. filegrc applies a 30-day deadline when a custom event obligation omits one.
|
|
145
131
|
|
|
146
132
|
Complete an event action and link its new proof in one validated write:
|
|
147
133
|
|
|
@@ -150,7 +136,7 @@ npx filegrc complete-action action-item-id completion-record.json --completed-on
|
|
|
150
136
|
npx filegrc complete-event obligation-event-id --completed-on 2026-07-25
|
|
151
137
|
```
|
|
152
138
|
|
|
153
|
-
|
|
139
|
+
filegrc rejects a completion resource whose type does not match the obligation. It will close the event only after every action has its requested proof.
|
|
154
140
|
|
|
155
141
|
## Headless Markdown
|
|
156
142
|
|
|
@@ -161,7 +147,7 @@ npx filegrc content risk-assessment risk-assessment-2026 --json
|
|
|
161
147
|
npx filegrc content risk-assessment risk-assessment-2026 --write updated-assessment.md
|
|
162
148
|
```
|
|
163
149
|
|
|
164
|
-
Run `filegrc guide <type>` to get slot names. Policies use `content`, meetings use `agenda` and `minutes`, and implicit long-form work uses `record`.
|
|
150
|
+
Run `filegrc guide <type>` to get slot names. Policies use `content`, meetings use `agenda` and `minutes`, and implicit long-form work uses `record`. filegrc derives the path and rejects content that does not belong to the record.
|
|
165
151
|
|
|
166
152
|
## Program readiness and the candidate period
|
|
167
153
|
|
|
@@ -169,7 +155,7 @@ Prepare the management program before creating an audit engagement:
|
|
|
169
155
|
|
|
170
156
|
```sh
|
|
171
157
|
npx filegrc program-readiness
|
|
172
|
-
npx filegrc program-readiness --require-ready --json
|
|
158
|
+
npx filegrc program-readiness --require-ready --summary --json
|
|
173
159
|
```
|
|
174
160
|
|
|
175
161
|
The Evidence Ready gate requires:
|
|
@@ -178,15 +164,15 @@ The Evidence Ready gate requires:
|
|
|
178
164
|
2. Active policies with completed text, separate management approval, real approval and effective dates, and linked controls.
|
|
179
165
|
3. Implemented controls with an owner, actual procedure, scope, cadence, evidence source, mappings, implementation date, and every eligible linked Work Queue schedule running.
|
|
180
166
|
4. Active authoritative systems with evidence source roles, access owners, and repeatable extraction instructions in Record Markdown.
|
|
181
|
-
5. A verified `test-export` or `test-capture` evidence record for each selected control family that relies on evidence from outside
|
|
167
|
+
5. A verified `test-export` or `test-capture` evidence record for each selected control family that relies on evidence from outside filegrc.
|
|
182
168
|
|
|
183
|
-
|
|
169
|
+
Onboarding does not create External Evidence records. After confirming the applicable controls and authoritative source Systems, run `npx filegrc evidence-test-drafts --preview --json` and review the proposed collection tests. Then run `npx filegrc evidence-test-drafts` to create the missing drafts. filegrc-managed records, such as risk assessments, meetings, vendor reviews, attestations, vulnerability scans, penetration tests, backup tests, exercises, exceptions, and findings, do not need a separate collection test. Put any fixed external artifact in an External Evidence record and link it from the operating record. For each created draft, choose its authoritative source System, attach or reference the real result, record its collector and classification, then have another person verify it.
|
|
184
170
|
|
|
185
171
|
When the gate passes, set `workspace.candidatePeriodStart` to the date reliable evidence collection begins. Do not backdate it. `candidatePeriodStart` and `candidatePeriodEnd` express management’s target. They do not establish the final report period.
|
|
186
172
|
|
|
187
173
|
Maintain risk assessments and the risk register while the program operates. Complete assessments on schedule and after material changes, and add or update controls when the conclusions require a different response. Audit preparation still checks for a current, independently reviewed assessment.
|
|
188
174
|
|
|
189
|
-
Record complementary customer or subservice controls after the internal control set is defined. `complementary-control.relatedControlIds` is the source of truth for those links.
|
|
175
|
+
Record complementary customer or subservice controls after the internal control set is defined. `complementary-control.relatedControlIds` is the source of truth for those links. filegrc derives the reverse connections for Control pages and evidence packets.
|
|
190
176
|
|
|
191
177
|
## Audit preparation and evidence packets
|
|
192
178
|
|
|
@@ -200,20 +186,20 @@ npx filegrc audit-readiness audit-2026-type-2 --require-ready --json
|
|
|
200
186
|
|
|
201
187
|
The audit record’s `typeOneAsOf`, `periodStart`, and `periodEnd` are the dates agreed with the CPA firm. Keep the workspace candidate dates even when the formal period differs.
|
|
202
188
|
|
|
203
|
-
|
|
189
|
+
{{audit_preparation_guidance}}
|
|
204
190
|
|
|
205
191
|
Review both evidence paths against the exact firm-agreed date or period:
|
|
206
192
|
|
|
207
|
-
1.
|
|
193
|
+
1. filegrc Evidence consists of dated Step 5 operating records. Complete each applicable record, link it to the Controls it supports, record the result in structured fields or Markdown, and link any external artifact needed to support that result.
|
|
208
194
|
2. External Evidence consists of verified `evidence` records from authoritative Systems. Confirm the source System, audit date or period, Control links, collector, verifier, and fixed attachment or approved external reference.
|
|
209
195
|
|
|
210
|
-
Audit Readiness reports coverage for both paths. The packet includes the matching
|
|
196
|
+
Audit Readiness reports coverage for both paths. The packet includes the matching filegrc records and Markdown with Git history, plus External Evidence records, retained attachments, delivery indexes, and checksums.
|
|
211
197
|
|
|
212
198
|
Near the end of fieldwork, link a verified fixed-format copy of the signed management representation letter to its engagement-specific document. Date it on or after the Type 1 date or Type 2 period end. A representation that is still marked for later blocks packet delivery.
|
|
213
199
|
|
|
214
200
|
Catalog each authoritative source under Systems and assign its `evidenceSourceKinds`. A third-party application is still a System because it operates controls or produces evidence. Create a separate Vendor for its provider and connect the System through `vendorId`; keep contracts, due diligence, and supplier risk on the Vendor. Name the people who can access system reports and keep extraction instructions in the System's Record Markdown. For each Type 2 population, select one source system and export the exact audit period. Split a population when different systems or queries produce its items. Link a verified `population-export` evidence record that names the same source system and stores the query or report parameters, generation time, timezone, count, completeness check, and accuracy check. A zero count still requires the source export and query. A population linked to an in-scope control cannot be marked not applicable.
|
|
215
201
|
|
|
216
|
-
Every evidence record names its collector. Verified evidence also names its verifier and verification date. Use `sourceSystemId` for system exports, `sourceResourceIds` for
|
|
202
|
+
Every evidence record names its collector. Verified evidence also names its verifier and verification date. Use `sourceSystemId` for system exports, `sourceResourceIds` for filegrc records, and `sourceCommit` to bind the evidence to repository state.
|
|
217
203
|
|
|
218
204
|
Preview coverage before writing output:
|
|
219
205
|
|
|
@@ -223,15 +209,15 @@ npx filegrc evidence-packet --audit audit-2026-type-2
|
|
|
223
209
|
npx filegrc evidence-packet --audit audit-2026-type-2 --preview --require-ready
|
|
224
210
|
```
|
|
225
211
|
|
|
226
|
-
The packet includes records explicitly related to the selected engagement, its systems, controls, criteria, policies, evidence, and dependencies. It does not include unrelated dated records from the workspace. A Type 2 packet adds
|
|
212
|
+
The packet includes records explicitly related to the selected engagement, its systems, controls, criteria, policies, evidence, and dependencies. It does not include unrelated dated records from the workspace. A Type 2 packet adds filegrc Evidence, recurring obligation occurrences, event workflows, and management population reconciliations. Output includes a control matrix with separate filegrc Evidence and External Evidence columns, source-system index, external-evidence delivery index, population index, evidence index, committed historical source versions, and SHA-256 checksums. Output under `.filegrc/evidence-packets/` is derived and must not be hand-edited or committed.
|
|
227
213
|
|
|
228
|
-
Treat a packet as ready for management delivery only when its status is `delivery-ready`, its review list is clear, and its manifest names a clean Git revision. This means
|
|
214
|
+
Treat a packet as ready for management delivery only when its status is `delivery-ready`, its review list is clear, and its manifest names a clean Git revision. This means filegrc's management checks passed. It does not mean the engagement team found the evidence sufficient or appropriate. The generator copies raw records, Markdown, and local fixed attachments. It never fetches external references. Reconcile `external-evidence-index.csv` to the auditor portal or other approved delivery system before telling the engagement team that submission is complete.
|
|
229
215
|
|
|
230
|
-
Link a control test to its `audit-population` record when sampling applies. Link item-level sample evidence separately. Management owns population completeness and accuracy. The auditor owns sample selection, independent testing, exception evaluation, and the report opinion. The auditor or publisher also supplies the authoritative criteria and examination guidance.
|
|
216
|
+
Link a control test to its `audit-population` record when sampling applies. Link item-level sample evidence separately. Management owns population completeness and accuracy. The auditor owns sample selection, independent testing, exception evaluation, and the report opinion. The auditor or publisher also supplies the authoritative criteria and examination guidance. filegrc stores references and orientation text, not licensed criteria.
|
|
231
217
|
|
|
232
218
|
## Content and approvals
|
|
233
219
|
|
|
234
|
-
The seed policy owner is {{policy_owner_name}} and the reporting address is {{security_contact_email}}. Replace ownership or contacts when responsibilities change.
|
|
220
|
+
The seed policy owner is {{policy_owner_name}} at {{policy_owner_email}}, and the security reporting address is {{security_contact_email}}. Replace ownership or contacts when responsibilities change.
|
|
235
221
|
|
|
236
222
|
Appoint an independent management reviewer during policy review, not as a condition of defining the service boundary. The reviewer must be separate from the policy owner and able to challenge the owner’s decisions. Most organizations assign another internal leader or manager. An external reviewer is also allowed, and a one-person company needs one because no second internal person is available. The reviewer chairs Security and Risk Oversight and approves policies and governed documents.
|
|
237
223
|
|