create-filegrc 0.3.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/README.md CHANGED
@@ -11,10 +11,37 @@ npm run serve
11
11
 
12
12
  Setup asks for the legal organization name, the initial policy owner and their email, a security reporting address, and the program timezone. The generated private project includes a starter Security program, Program Readiness, a policy-driven Work Queue, Policy Events that add linked tasks, later audit preparation, evidence packets, and one dependency: `filegrc`.
13
13
 
14
+ Creation has two layers. The five-record foundation contains workspace settings, the initial owner, the oversight team, renderer settings, and the filegrc system of record. The default `security` starter adds the framework references, proposed policies, controls, obligations, documents, and training records. Pass `--starter foundation` when you want to stop before selecting a framework.
15
+
16
+ For one noninteractive run, pass company and service fields together or use `--config setup.json`. The optional `setup` object defines the service boundary and management goal after installation. Use `--filegrc-package <directory>` to exercise unpublished local engine changes instead of installing the registry release. This writes a machine-local `file:` dependency, so replace it with a released version before sharing the generated workspace.
17
+
18
+ ```json
19
+ {
20
+ "companyName": "Example Company",
21
+ "policyOwnerName": "Security Owner",
22
+ "policyOwnerEmail": "owner@example.com",
23
+ "securityContactEmail": "security@example.com",
24
+ "timezone": "America/Chicago",
25
+ "starter": "security",
26
+ "setup": {
27
+ "serviceName": "Example Service",
28
+ "boundary": "The production service and supporting infrastructure.",
29
+ "criticality": "high",
30
+ "dataClassification": "Confidential",
31
+ "internetExposed": true,
32
+ "programGoal": "type-2"
33
+ }
34
+ }
35
+ ```
36
+
37
+ ```sh
38
+ npx create-filegrc@latest company-grc --config setup.json
39
+ ```
40
+
14
41
  Generated workspaces also include layered `AGENTS.md` instructions and model-driven headless commands. `filegrc program-path` gives agents the same six steps, exact page guidance, current status, and next actions shown in the renderer. Agents can define program scope, approve policies, implement controls, test External Evidence, complete policy work, trigger event tasks, and prepare later audit packets through the same domain functions used by the renderer.
15
42
 
16
43
  Git is initialized when needed. The browser can create local commits before a remote is configured.
17
44
 
18
45
  Creation output reports the resolved filegrc version, program timezone, starter record counts, whether installation ran, and whether the target joined an existing Git worktree or received a new repository.
19
46
 
20
- Use `npx create-filegrc@latest --help` for non-interactive options.
47
+ Use `npx create-filegrc@latest --help` for noninteractive options.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-filegrc",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Create a filegrc workspace for a SOC 2 program",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -1,17 +1,42 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
2
3
  import { createFilegrc } from "./index.js";
3
4
 
4
5
  export async function runCli(argv = process.argv.slice(2)) {
5
- const { target, options } = parseArgs(argv);
6
+ const parsed = parseArgs(argv);
7
+ const config = parsed.options.config ? await readConfig(parsed.options.config) : {};
8
+ const organization = config.organization && typeof config.organization === "object"
9
+ ? config.organization
10
+ : {};
11
+ const options = {
12
+ ...organization,
13
+ ...config,
14
+ ...parsed.options,
15
+ setup: {
16
+ ...(config.setup || {}),
17
+ ...parsed.setup
18
+ }
19
+ };
20
+ delete options.organization;
21
+ delete options.config;
22
+ delete options.target;
23
+ if (!Object.keys(options.setup).length) delete options.setup;
24
+ const target = parsed.target ?? config.target ?? "filegrc-program";
6
25
  if (options.help) return printHelp();
7
26
  if (options.version) return printVersion();
8
27
  const result = await createFilegrc({ target, ...options });
9
28
  console.log(`Created ${result.target}`);
10
- console.log(`filegrc ${result.engineVersion}: ${result.install === "installed" ? "installed" : "installation skipped"}`);
29
+ console.log(
30
+ `filegrc ${result.engineVersion}${result.enginePackage ? ` from ${result.enginePackage}` : ""}: ` +
31
+ `${result.install === "installed" ? "installed" : "installation skipped"}`
32
+ );
11
33
  console.log(`Git: ${result.gitMode === "existing-worktree" ? "joined existing worktree" : "initialized new repository"}`);
12
34
  console.log(`Timezone: ${result.values.timezone}`);
35
+ for (const stage of result.stages) {
36
+ console.log(`Stage ${stage.id}: ${stage.status}${stage.status === "created" ? ` (${stage.records} records)` : ""}`);
37
+ }
13
38
  console.log(
14
- `Program baseline: ${result.resourceCounts.total} records, including ` +
39
+ `${result.setup ? "Workspace after setup" : "Program baseline"}: ${result.resourceCounts.total} records, including ` +
15
40
  `${result.resourceCounts.byType.requirement || 0} requirements, ` +
16
41
  `${result.resourceCounts.byType.control || 0} controls, and ` +
17
42
  `${result.resourceCounts.byType.obligation || 0} obligations.`
@@ -19,11 +44,14 @@ export async function runCli(argv = process.argv.slice(2)) {
19
44
  console.log("");
20
45
  console.log(` cd ${shellQuote(result.target)}`);
21
46
  if (options.install === false) console.log(" npm install");
22
- console.log(" npx filegrc setup");
47
+ if (!result.setup) console.log(" npx filegrc setup");
23
48
  console.log(" npm run validate");
24
49
  console.log(" npm run serve");
25
50
  console.log("");
26
- console.log("Review the starter drafts, then commit the approved baseline.");
51
+ if (result.setup) {
52
+ console.log(`Service setup: ${result.setup.system.id} (${result.setup.system.status}), target ${result.setup.target.assuranceGoal}.`);
53
+ }
54
+ console.log("Review the generated records, then commit the approved baseline.");
27
55
  }
28
56
 
29
57
  function shellQuote(value) {
@@ -33,6 +61,7 @@ function shellQuote(value) {
33
61
  function parseArgs(argv) {
34
62
  let target;
35
63
  const options = {};
64
+ const setup = {};
36
65
  for (let index = 0; index < argv.length; index += 1) {
37
66
  const value = argv[index];
38
67
  if (!value.startsWith("-") && !target) {
@@ -46,15 +75,26 @@ function parseArgs(argv) {
46
75
  else if (name === "yes" || name === "y") options.yes = true;
47
76
  else if (name === "force") options.force = true;
48
77
  else if (name === "no-install") options.install = false;
78
+ else if (name === "config") options.config = next();
49
79
  else if (name === "company-name") options.companyName = next();
50
80
  else if (name === "policy-owner-name") options.policyOwnerName = next();
51
81
  else if (name === "policy-owner-email") options.policyOwnerEmail = next();
52
82
  else if (name === "security-contact-email") options.securityContactEmail = next();
53
83
  else if (name === "timezone") options.timezone = next();
84
+ else if (name === "starter") options.starter = next();
54
85
  else if (name === "filegrc-version") options.filegrcVersion = next();
86
+ else if (name === "filegrc-package") options.filegrcPackage = next();
87
+ else if (name === "service-name") setup.serviceName = next();
88
+ else if (name === "boundary") setup.boundary = next();
89
+ else if (name === "service-owner") setup.ownerId = next();
90
+ else if (name === "criticality") setup.criticality = next();
91
+ else if (name === "classification") setup.dataClassification = next();
92
+ else if (name === "internet-exposed") setup.internetExposed = booleanOption(next(), "internet-exposed");
93
+ else if (name === "program-goal") setup.programGoal = next();
94
+ else if (name === "setup-draft") setup.draft = true;
55
95
  else throw new Error(`Unknown option "${value}".`);
56
96
  }
57
- return { target: target ?? "filegrc-program", options };
97
+ return { target, options, setup };
58
98
  }
59
99
 
60
100
  function printHelp() {
@@ -63,12 +103,23 @@ function printHelp() {
63
103
  Create a filegrc workspace for a SOC 2 program.
64
104
 
65
105
  Options:
106
+ --config <json-file|-> Read creation and optional setup values
66
107
  --company-name <legal-name> Legal organization name
67
108
  --policy-owner-name <name> Initial policy owner
68
109
  --policy-owner-email <email> Policy owner's email address
69
110
  --security-contact-email <email> Security reporting address
70
111
  --timezone <iana-timezone> Program timezone, such as America/Chicago
112
+ --starter <profile> security (default) or foundation
71
113
  --filegrc-version <version> Override resolved engine version
114
+ --filegrc-package <directory> Install an unpublished local filegrc package
115
+ --service-name <name> Complete service setup after creation
116
+ --boundary <description> Initial service boundary
117
+ --service-owner <person-id> Defaults to person-policy-owner
118
+ --criticality <level> low, medium, high, or critical
119
+ --classification <name> Initial data classification
120
+ --internet-exposed <bool> true or false
121
+ --program-goal <goal> none, readiness, type-1, or type-2
122
+ --setup-draft Save combined service setup as a draft
72
123
  --no-install Write files and a preliminary lockfile only
73
124
  --force Allow a non-empty target without overwriting files
74
125
  --yes Use generic prompt defaults
@@ -80,3 +131,26 @@ async function printVersion() {
80
131
  const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
81
132
  console.log(packageJson.version);
82
133
  }
134
+
135
+ async function readConfig(path) {
136
+ const source = path === "-"
137
+ ? await readStdin()
138
+ : await readFile(resolve(String(path)), "utf8");
139
+ const parsed = JSON.parse(source);
140
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
141
+ throw new Error("Creation config must be a JSON object.");
142
+ }
143
+ return parsed;
144
+ }
145
+
146
+ async function readStdin() {
147
+ const chunks = [];
148
+ for await (const chunk of process.stdin) chunks.push(chunk);
149
+ return Buffer.concat(chunks).toString("utf8");
150
+ }
151
+
152
+ function booleanOption(value, name) {
153
+ if (value === "true") return true;
154
+ if (value === "false") return false;
155
+ throw new Error(`--${name} must be true or false.`);
156
+ }
package/src/defaults.js CHANGED
@@ -884,11 +884,11 @@ const obligations = [
884
884
  }
885
885
  ];
886
886
 
887
- export function baselineRecordPaths() {
888
- return baselineRecordFiles("2000-01-01").map(({ path }) => path);
887
+ export function baselineRecordPaths(starter = "security") {
888
+ return baselineRecordFiles("2000-01-01", starter).map(({ path }) => path);
889
889
  }
890
890
 
891
- export function baselineRecordFiles(effectiveDate) {
891
+ export function baselineRecordFiles(effectiveDate, starter = "security") {
892
892
  const framework = {
893
893
  schemaVersion: 1,
894
894
  id: FRAMEWORK_ID,
@@ -1006,20 +1006,25 @@ export function baselineRecordFiles(effectiveDate) {
1006
1006
  policyIds: obligation.policyIds
1007
1007
  }));
1008
1008
 
1009
+ const foundation = [
1010
+ recordFile("systems", programRepository),
1011
+ recordFile("teams", team)
1012
+ ];
1013
+ if (starter === "foundation") return foundation;
1014
+ if (starter !== "security") throw new Error(`Unknown starter profile "${starter}".`);
1009
1015
  return [
1010
1016
  recordFile("frameworks", framework),
1011
1017
  recordFile("frameworks", descriptionFramework),
1012
1018
  ...commonRequirements.map((record) => recordFile("requirements", record)),
1013
1019
  ...descriptionRequirements.map((record) => recordFile("requirements", record)),
1014
1020
  ...controlRecords.map((record) => recordFile("controls", record)),
1015
- recordFile("systems", programRepository),
1016
- recordFile("teams", team),
1021
+ ...foundation,
1017
1022
  ...obligationRecords.map((record) => recordFile("obligations", record))
1018
1023
  ];
1019
1024
  }
1020
1025
 
1021
- export async function writeBaselineRecords(target, effectiveDate) {
1022
- for (const { path: relativePath, record } of baselineRecordFiles(effectiveDate)) {
1026
+ export async function writeBaselineRecords(target, effectiveDate, starter = "security") {
1027
+ for (const { path: relativePath, record } of baselineRecordFiles(effectiveDate, starter)) {
1023
1028
  const path = join(target, relativePath);
1024
1029
  await mkdir(dirname(path), { recursive: true });
1025
1030
  await writeFile(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
package/src/index.js CHANGED
@@ -4,33 +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
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 engineVersion = await resolveFilegrcVersion(options.filegrcVersion);
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: engineVersion,
26
- filegrc_version_range: `^${engineVersion}`
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);
33
- const resourceCounts = await summarizeResources(target);
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);
34
44
 
35
45
  const installed = options.install !== false;
36
46
  if (installed) {
@@ -40,16 +50,33 @@ export async function createFilegrc(options = {}) {
40
50
  }
41
51
  const joinedExistingWorktree = await isInsideGitWorktree(target);
42
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;
43
55
  return {
44
56
  target,
45
57
  values,
46
- engineVersion,
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),
47
64
  resourceCounts,
65
+ setup,
48
66
  install: installed ? "installed" : "skipped",
49
67
  gitMode: joinedExistingWorktree ? "existing-worktree" : "initialized"
50
68
  };
51
69
  }
52
70
 
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
+
53
80
  export async function resolveFilegrcVersion(explicitVersion) {
54
81
  if (explicitVersion) return cleanVersion(explicitVersion);
55
82
  try {
@@ -65,6 +92,28 @@ export async function resolveFilegrcVersion(explicitVersion) {
65
92
  }
66
93
  }
67
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
+
68
117
  async function resolvePromptValues(parameters, options) {
69
118
  const mapped = {
70
119
  company_name: options.companyName,
@@ -161,9 +210,9 @@ async function assertWritableTarget(target, force) {
161
210
  }
162
211
  }
163
212
 
164
- async function assertNoTemplateCollisions(target) {
213
+ async function assertNoTemplateCollisions(target, starter) {
165
214
  const collisions = [];
166
- for (const destinationPath of [...await templateDestinationPaths(), ...baselineRecordPaths()]) {
215
+ for (const destinationPath of [...await templateDestinationPaths(starter), ...baselineRecordPaths(starter)]) {
167
216
  await assertNoSymlinkComponents(target, destinationPath);
168
217
  try {
169
218
  await lstat(join(target, destinationPath));
@@ -192,20 +241,22 @@ async function assertNoSymlinkComponents(target, relativePath) {
192
241
  }
193
242
  }
194
243
 
195
- async function renderTemplate(target, parameterConfig, values) {
244
+ async function renderTemplate(target, parameterConfig, values, starter) {
196
245
  const declared = new Set([
197
246
  ...parameterConfig.parameters.map(({ key }) => key),
198
247
  ...parameterConfig.generated.map(({ key }) => key)
199
248
  ]);
200
- const files = (await templateDestinationPaths()).map((path) => join(target, path));
249
+ const files = (await templateDestinationPaths(starter)).map((path) => join(target, path));
201
250
  for (const path of files) {
202
251
  if (!textExtensions.has(extname(path)) && basename(path) !== ".gitignore") continue;
203
252
  let source = await readFile(path, "utf8");
204
253
  const jsonFile = extname(path) === ".json";
205
- source = source.replace(/\{\{([a-z0-9_]+)\}\}/g, (match, token) => {
254
+ source = source.replace(/\{\{([a-z0-9_]+)\}\}(\.)?/g, (match, token, sentencePeriod = "") => {
206
255
  if (!declared.has(token)) throw new Error(`Unknown template token "{{${token}}}" in ${path}`);
207
256
  if (values[token] === undefined) throw new Error(`No value resolved for template token "{{${token}}}"`);
208
- return jsonFile ? jsonStringContents(values[token]) : String(values[token]);
257
+ const value = jsonFile ? jsonStringContents(values[token]) : String(values[token]);
258
+ const punctuation = !jsonFile && sentencePeriod && /[.!?]$/u.test(value) ? "" : sentencePeriod;
259
+ return value + punctuation;
209
260
  });
210
261
  const unresolved = /\{\{([a-z0-9_]+)\}\}/.exec(source);
211
262
  if (unresolved) throw new Error(`Unresolved template token "{{${unresolved[1]}}}" in ${path}`);
@@ -213,20 +264,22 @@ async function renderTemplate(target, parameterConfig, values) {
213
264
  }
214
265
  }
215
266
 
216
- async function templateDestinationPaths() {
267
+ async function templateDestinationPaths(starter = "security") {
217
268
  const template = join(packageRoot, "template");
218
269
  return (await collectFiles(template)).flatMap((source) => {
219
270
  const templatePath = relative(template, source);
271
+ if (!includeTemplatePath(templatePath, starter)) return [];
220
272
  if (templatePath === "README.md") return [];
221
273
  if (templatePath === "WORKSPACE.md") return ["README.md"];
222
274
  return [templatePath === "gitignore" ? ".gitignore" : templatePath];
223
275
  });
224
276
  }
225
277
 
226
- async function copyTemplate(target) {
278
+ async function copyTemplate(target, starter = "security") {
227
279
  const template = join(packageRoot, "template");
228
280
  for (const source of await collectFiles(template)) {
229
281
  const templatePath = relative(template, source);
282
+ if (!includeTemplatePath(templatePath, starter)) continue;
230
283
  if (templatePath === "README.md") continue;
231
284
  const destinationPath = templatePath === "WORKSPACE.md"
232
285
  ? "README.md"
@@ -242,6 +295,13 @@ async function copyTemplate(target) {
242
295
  }
243
296
  }
244
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
+
245
305
  async function collectFiles(directory) {
246
306
  const result = [];
247
307
  for (const item of await readdir(directory, { withFileTypes: true })) {
@@ -265,16 +325,137 @@ async function summarizeResources(target) {
265
325
  return { total, byType: counts };
266
326
  }
267
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
+
268
449
  async function writeMinimalLockfile(target, name, versionRange) {
269
450
  const lock = {
270
451
  name,
271
- version: "0.3.0",
452
+ version: "0.3.1",
272
453
  lockfileVersion: 3,
273
454
  requires: true,
274
455
  packages: {
275
456
  "": {
276
457
  name,
277
- version: "0.3.0",
458
+ version: "0.3.1",
278
459
  dependencies: { filegrc: versionRange }
279
460
  }
280
461
  }
@@ -1,8 +1,8 @@
1
- # filegrc SOC 2 Workspace Instructions
1
+ # {{agent_title}}
2
2
 
3
3
  ## Purpose
4
4
 
5
- This repository is {{company_name}}’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.
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
 
@@ -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 a `system` record and stores the management goal and program scope on `workspace`. 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.
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
- ## Starter baseline
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
 
@@ -180,7 +166,7 @@ The Evidence Ready gate requires:
180
166
  4. Active authoritative systems with evidence source roles, access owners, and repeatable extraction instructions in Record Markdown.
181
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
- Completing onboarding creates draft External Evidence records only for evidence that must come from other systems and does not already have a dedicated Step 5 record. 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 generated draft, choose its authoritative source System, attach or reference the real result, record its collector and classification, then have another person verify it. Run `npx filegrc evidence-test-drafts` to create any drafts needed after the control set changes.
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
 
@@ -200,7 +186,7 @@ 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
- 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.
189
+ {{audit_preparation_guidance}}
204
190
 
205
191
  Review both evidence paths against the exact firm-agreed date or period:
206
192
 
@@ -34,6 +34,8 @@ npm run validate
34
34
  npm run serve
35
35
  ```
36
36
 
37
+ The default `security` starter builds the full proposed SOC 2 Security program. Use `--starter foundation` to create only the five structural records when you want to select a framework and program content later. Company and service values can also be supplied together through `create-filegrc --config setup.json`.
38
+
37
39
  Setup asks for the legal organization name, the initial policy owner and their email, a security reporting address, and the program timezone. It initializes Git when needed. The first local run then defines the initial service boundary and an optional program goal. A Type 2 choice records management intent, not an audit engagement. Completing onboarding opens Step 1 so you can add the real reviewers and operators, finish the oversight team, and confirm the criteria, commitments, vendors, and systems before moving on.
38
40
 
39
41
  Open the printed local URL. You can commit locally from Repository without configuring a remote. Add a remote when the team is ready to share the workspace, then the browser can pull with rebase and push reviewed commits.
@@ -45,7 +47,7 @@ The creation summary reports the resolved engine version, program timezone, star
45
47
  1. Confirm the program’s people and oversight team, applicable criteria, commitments, material vendors, and in-scope systems.
46
48
  2. Review and activate the policies with a separate management reviewer, who is usually internal and may be external.
47
49
  3. Tailor the starter controls, add each owner, actual procedure, scope, cadence, evidence source, 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.
48
- 4. Open each generated External Evidence draft, choose its authoritative source System, collect the named artifact, and have another person verify it.
50
+ 4. After confirming the applicable controls and authoritative source Systems, preview the proposed External Evidence drafts with `npx filegrc evidence-test-drafts --preview --json`. Create only the relevant drafts, collect each named artifact, and have another person verify it.
49
51
  5. Start the management candidate period, maintain risk assessments and risks, update controls when needed, work the filegrc queue, and preserve dated evidence.
50
52
  6. Engage a CPA firm, record the separate firm-agreed period, review filegrc Evidence and External Evidence, prepare fieldwork, and generate the evidence packet.
51
53
 
@@ -1,8 +1,8 @@
1
- # {{company_name}} SOC 2 Program
1
+ # {{program_title}}
2
2
 
3
- This private workspace holds {{company_name}}'s SOC 2 program records and audit evidence. JSON under `data/` stores structured records, Markdown stores long-form work, and Git records reviewed changes.
3
+ {{program_summary}}
4
4
 
5
- The workspace uses filegrc {{filegrc_version}} through the dependency range `{{filegrc_version_range}}`.
5
+ The workspace uses filegrc {{filegrc_version}} through the dependency spec `{{filegrc_version_range}}`.
6
6
 
7
7
  ## Work locally
8
8
 
@@ -27,15 +27,6 @@ npx filegrc validate --json
27
27
 
28
28
  Read `AGENTS.md` and `data/AGENTS.md` before broad changes.
29
29
 
30
- ## Finish initial setup
31
-
32
- The starter policies, controls, and obligations are proposals. They do not state that {{company_name}} operates the described controls.
33
-
34
- 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.
35
- 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.
36
- 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.
37
- 4. Open each generated External Evidence draft, choose its authoritative source System, collect the named artifact, and have another person verify it.
38
- 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. `npx filegrc obligations` previews every event task, owner, deadline, and requested proof before the trigger creates anything.
39
- 6. Engage a CPA firm, record the separate firm-agreed period in an audit record, review filegrc Evidence and External Evidence, and prepare fieldwork.
30
+ {{starter_setup}}
40
31
 
41
32
  filegrc manages GRC records and audit evidence. It does not replace infrastructure logging, monitoring, identity, backup, endpoint, or incident-detection systems.
@@ -2,7 +2,7 @@
2
2
 
3
3
  An evidence record explains what a proof item is, where it came from, what period it supports, who collected it, and which records or controls it supports. The attachment alone is not enough.
4
4
 
5
- Completing onboarding creates draft collection tests only for evidence that must come from systems outside filegrc and does not already have a dedicated Step 5 record. Risk assessments, meetings, vendor reviews, attestations, vulnerability scans, penetration tests, backup tests, exercises, exceptions, and findings do not need a separate test. When one of those operating records needs a fixed external artifact, create or update an External Evidence record for the artifact and link its ID from the operating record. Keep a generated collection test as `draft` until the artifact has actually been captured. Set it to `collected` only after selecting the source System, attaching or referencing the result, and recording the source, date, classification, and collector. Set it to `verified` only after another named person checks it.
5
+ Onboarding does not create collection tests. After confirming applicable controls and authoritative source Systems, preview proposed tests with `npx filegrc evidence-test-drafts --preview --json`, then explicitly create the missing drafts. Risk assessments, meetings, vendor reviews, attestations, vulnerability scans, penetration tests, backup tests, exercises, exceptions, and findings do not need a separate test. When one of those operating records needs a fixed external artifact, create or update an External Evidence record for the artifact and link its ID from the operating record. Keep a created collection test as `draft` until the artifact has actually been captured. Set it to `collected` only after selecting the source System, attaching or referencing the result, and recording the source, date, classification, and collector. Set it to `verified` only after another named person checks it.
6
6
 
7
7
  ## Create evidence
8
8
 
@@ -3,10 +3,10 @@
3
3
  "dataModelVersion": "1",
4
4
  "id": "workspace",
5
5
  "type": "workspace",
6
- "title": "{{company_name}} SOC 2 Program",
6
+ "title": "{{program_title}}",
7
7
  "organizationName": "{{company_name}}",
8
8
  "timezone": "{{timezone}}",
9
- "description": "SOC 2 Security program based on the AICPA Trust Services Criteria.",
9
+ "description": "{{program_description}}",
10
10
  "riskMethodology": {
11
11
  "method": "5x5 likelihood and impact",
12
12
  "likelihoodScale": ["Rare", "Unlikely", "Possible", "Likely", "Almost certain"],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "{{project_name}}",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "description": "filegrc workspace for a SOC 2 program",
6
6
  "type": "module",
@@ -45,6 +45,38 @@
45
45
  {
46
46
  "key": "filegrc_version_range",
47
47
  "source": "resolved-filegrc-version"
48
+ },
49
+ {
50
+ "key": "program_title",
51
+ "source": "starter-profile"
52
+ },
53
+ {
54
+ "key": "program_description",
55
+ "source": "starter-profile"
56
+ },
57
+ {
58
+ "key": "program_summary",
59
+ "source": "starter-profile"
60
+ },
61
+ {
62
+ "key": "starter_setup",
63
+ "source": "starter-profile"
64
+ },
65
+ {
66
+ "key": "agent_title",
67
+ "source": "starter-profile"
68
+ },
69
+ {
70
+ "key": "agent_purpose",
71
+ "source": "starter-profile"
72
+ },
73
+ {
74
+ "key": "starter_baseline",
75
+ "source": "starter-profile"
76
+ },
77
+ {
78
+ "key": "audit_preparation_guidance",
79
+ "source": "starter-profile"
48
80
  }
49
81
  ]
50
82
  }