filegrc 0.16.5 → 0.16.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.5",
3
+ "version": "0.16.7",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,70 @@
1
+ // Keep a short, source-backed inventory beside a derived next action. A link
2
+ // means a record exists; it never proves that the recorded process operated.
3
+ export function buildActionContext(loaded, item) {
4
+ const records = loaded.resources;
5
+ const byId = new Map(records.map((record) => [record.id, record]));
6
+ const subjectId = item.subject?.id || item.resourceId;
7
+ const subject = subjectId ? byId.get(subjectId) : null;
8
+ const found = new Map();
9
+ const add = (id, relationship) => {
10
+ const record = byId.get(id);
11
+ if (!record || found.has(id)) return;
12
+ found.set(id, {
13
+ type: record.type,
14
+ id: record.id,
15
+ title: record.title,
16
+ status: record.status ?? null,
17
+ relationship
18
+ });
19
+ };
20
+ if (subject) add(subject.id, "subject");
21
+ if (item.source?.id) add(item.source.id, "source");
22
+ for (const id of item.sourceResourceIds || []) add(id, "source");
23
+ for (const dependency of item.dependencies || []) if (dependency.id) add(dependency.id, "prerequisite");
24
+ const matchingType = item.createResourceType || (!subject && (item.subject?.type || item.resourceType));
25
+ const sameType = matchingType
26
+ ? records.filter((record) => record.type === matchingType)
27
+ : [];
28
+ if (subject) {
29
+ const fields = { ...loaded.model.commonFields, ...loaded.model.resources[subject.type]?.fields };
30
+ for (const [name, definition] of Object.entries(fields)) {
31
+ if (!definition?.relation) continue;
32
+ const ids = Array.isArray(subject[name]) ? subject[name] : [subject[name]];
33
+ for (const id of ids) if (typeof id === "string") add(id, name);
34
+ }
35
+ for (const record of records) {
36
+ if (record.id === subject.id) continue;
37
+ const incomingFields = { ...loaded.model.commonFields, ...loaded.model.resources[record.type]?.fields };
38
+ for (const [name, definition] of Object.entries(incomingFields)) {
39
+ if (!definition?.relation) continue;
40
+ const ids = Array.isArray(record[name]) ? record[name] : [record[name]];
41
+ if (ids.includes(subject.id)) add(record.id, name);
42
+ }
43
+ }
44
+ }
45
+ for (const record of sameType) add(record.id, "same-type-candidate");
46
+ const existing = [...found.values()];
47
+ const unmetChecks = item.unmetChecks || Object.entries(item.checks || {})
48
+ .filter(([, passed]) => passed === false)
49
+ .map(([name]) => name);
50
+ return {
51
+ workPhase: actionPhase(item),
52
+ operation: item.createResourceType
53
+ ? sameType.length ? "inspect-before-create" : "check-before-create"
54
+ : subject ? "inspect-existing" : sameType.length ? "inspect-before-create" : "check-before-create",
55
+ unmetChecks,
56
+ existing: existing.slice(0, 20),
57
+ existingCount: existing.length,
58
+ existingTruncated: existing.length > 20,
59
+ ...(matchingType ? { sameTypeCount: sameType.length } : {}),
60
+ operationProof: "not-inferred-from-links",
61
+ reminder: "Related records show current design or recorded work. Verify actual operation before claiming it occurred."
62
+ };
63
+ }
64
+
65
+ function actionPhase(item) {
66
+ if (item.assessment === "period-health" || ["run", "operation"].includes(item.stage)) return "operation";
67
+ if (["audit-readiness", "delivery-readiness", "audit-closure"].includes(item.assessment) || item.stage === "audit") return "audit";
68
+ if (item.assessment === "program-configuration" || ["scope", "policies", "controls"].includes(item.stage)) return "setup";
69
+ return "record";
70
+ }
package/src/agent.js CHANGED
@@ -74,6 +74,9 @@ export function buildAgentGuide(loaded, type, options = {}) {
74
74
  const location = definition.singleton
75
75
  ? `data/${definition.singleton}`
76
76
  : `data/${definition.collection}/${(definition.recordPath ?? "{id}.json").replaceAll("{id}", options.id || "{id}")}`;
77
+ const existingRecords = loaded.resources
78
+ .filter((item) => item.type === type)
79
+ .sort((left, right) => `${left.title}:${left.id}`.localeCompare(`${right.title}:${right.id}`));
77
80
 
78
81
  return {
79
82
  type,
@@ -104,6 +107,13 @@ export function buildAgentGuide(loaded, type, options = {}) {
104
107
  : null
105
108
  },
106
109
  location,
110
+ existingRecordCount: existingRecords.length,
111
+ existingRecords: existingRecords.slice(0, 25).map(({ id, title, status }) => ({
112
+ id,
113
+ title,
114
+ status: status ?? null
115
+ })),
116
+ existingRecordsTruncated: existingRecords.length > 25,
107
117
  singleton: Boolean(definition.singleton),
108
118
  requiredAtCreation,
109
119
  conditionalRequirements,
@@ -113,8 +123,8 @@ export function buildAgentGuide(loaded, type, options = {}) {
113
123
  workflow: [
114
124
  "Inspect existing records and relation candidates before writing.",
115
125
  definition.singleton
116
- ? "Open the existing singleton record, then replace every null value and empty required array with facts from an authoritative source."
117
- : "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
126
+ ? "Open the existing singleton record and update only gaps supported by facts from an authoritative source."
127
+ : "Reuse and update a suitable existing record. Create a scaffold only when no record covers the work, then replace every null value and empty required array with facts from an authoritative source.",
118
128
  recommendedMarkdown.length
119
129
  ? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
120
130
  : "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { ACTIVE_MODEL_VERSION, loadModel, SUPPORTED_MODEL_VERSIONS } from "../model/index.js";
5
5
  import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
6
+ import { buildActionContext } from "./action-context.js";
6
7
  import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
7
8
  import { saveAuditPopulation, scaffoldAuditPopulationCorrection } from "./audit-populations.js";
8
9
  import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
@@ -295,7 +296,7 @@ export async function runCli(argv = process.argv.slice(2)) {
295
296
  programId
296
297
  });
297
298
  const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
298
- const output = selectProgramPathOutput(result, flags);
299
+ const output = selectProgramPathOutput(result, flags, loaded);
299
300
  if (flags.json) console.log(JSON.stringify(output, null, 2));
300
301
  else printProgramPathOutput(output, flags);
301
302
  return output;
@@ -935,7 +936,7 @@ export async function runCli(argv = process.argv.slice(2)) {
935
936
  const output = flags.workflow
936
937
  ? {
937
938
  record,
938
- workflow: workflowForResource(await assessWorkflow(loaded), record.type, record.id)
939
+ workflow: workflowForResource(await assessWorkflow(loaded), record.type, record.id, loaded)
939
940
  }
940
941
  : record;
941
942
  console.log(JSON.stringify(output, null, 2));
@@ -1773,6 +1774,9 @@ function printAgentGuide(result) {
1773
1774
  console.log(`Policy basis: ${result.policyBasis}`);
1774
1775
  console.log(`Timing: ${result.cadence}`);
1775
1776
  console.log(`JSON: ${result.location}`);
1777
+ console.log(`Existing ${result.pluralTitle}: ${result.existingRecordCount}`);
1778
+ for (const record of result.existingRecords) console.log(`- ${record.id}\t${record.title}\t${record.status || ""}`);
1779
+ if (result.existingRecordsTruncated) console.log("- More records exist. Run filegrc list for the complete set.");
1776
1780
  if (result.reviewRequirements.collectionReview) {
1777
1781
  const review = result.reviewRequirements.collectionReview;
1778
1782
  console.log(`\nCollection review: ${review.title} (${review.status}, ${review.recordCount} ${review.recordCount === 1 ? "record" : "records"})`);
@@ -1924,11 +1928,11 @@ function printProgramPath(result) {
1924
1928
  }
1925
1929
  }
1926
1930
 
1927
- function selectProgramPathOutput(result, flags) {
1931
+ function selectProgramPathOutput(result, flags, loaded) {
1928
1932
  const modes = ["summary", "next", "current"].filter((name) => flags[name]);
1929
1933
  if (modes.length > 1) throw new Error("Use only one of --summary, --next, or --current.");
1930
1934
  if (flags.summary) return summarizeProgramPath(result);
1931
- if (flags.next) return nextProgramPath(result);
1935
+ if (flags.next) return nextProgramPath(result, loaded);
1932
1936
  if (flags.current) {
1933
1937
  const stage = result.stages.find(({ id }) => id === result.currentStep.id);
1934
1938
  return { ...result, stages: stage ? [stage] : [] };
@@ -1957,7 +1961,7 @@ function summarizeProgramPath(result) {
1957
1961
  };
1958
1962
  }
1959
1963
 
1960
- function nextProgramPath(result) {
1964
+ function nextProgramPath(result, loaded) {
1961
1965
  const stage = result.stages.find(({ id }) => id === result.currentStep.id);
1962
1966
  const nextAction = stage?.nextActions[0];
1963
1967
  return {
@@ -1975,7 +1979,9 @@ function nextProgramPath(result) {
1975
1979
  title: stage.title,
1976
1980
  status: stage.status,
1977
1981
  summary: stage.summary,
1978
- nextAction: summarizePathAction(nextAction),
1982
+ nextAction: nextAction
1983
+ ? { ...summarizePathAction(nextAction), context: buildActionContext(loaded, { ...nextAction, stage: stage.id }) }
1984
+ : null,
1979
1985
  commands: nextActionCommands(stage, nextAction)
1980
1986
  } : null
1981
1987
  };
@@ -120,6 +120,8 @@ Removable media containing Confidential or Restricted data requires owner approv
120
120
 
121
121
  The Data Retention Schedule defines the approved period and disposal method for important in-scope record classes. Supporting standards, procedures, and system records document implementation. Disposal must be suitable for the media and classification, with dated proof when the Control requires it.
122
122
 
123
+ Owners document and test how approved deletion requests and retention cutoffs apply to primary data, derived copies, local copies, Vendor-held data, and backups. Procedures identify copies that cannot be removed immediately, their access restrictions and expiry, and any legal hold or contractual limit. Owners verify completion against the approved scope before representing data as deleted.
124
+
123
125
  ## Cryptography, Encryption, Key, and Secrets Management Policy
124
126
 
125
127
  ### Encryption requirements
@@ -164,6 +166,8 @@ Passwords and other authenticators must meet settings approved for the System's
164
166
  - **Customer and external-user access:** MFA is required when an approved Control, customer commitment, or risk decision requires it.
165
167
  - **Exceptions:** Where required MFA is unavailable, management must approve a time-bound Exception with a risk assessment, compensating Controls, an accountable owner, and a review or expiration date.
166
168
 
169
+ Owners document customer authentication and workforce authentication separately for each relevant System. They record whether either path uses single sign-on, where MFA is enforced, and how access is revoked. Remote-access methods are selected and approved for the System and risk; a VPN is required only when an approved Control, commitment, or risk decision calls for one.
170
+
167
171
  ## Endpoint, Mobile Device, BYOD, and Malware Protection Policy
168
172
 
169
173
  ### Company devices and platform protection
@@ -194,6 +198,8 @@ Owners restrict inbound, outbound, and internal network paths and management int
194
198
 
195
199
  Production, development, test, and general-user environments must be separated to the extent needed for their data, exposure, privileges, and change risk. Connections between environments require approved paths and safeguards. Wireless and other local networks used for company work require authentication and encryption appropriate to current risk and technical capability.
196
200
 
201
+ When a service processes data for multiple customers, owners define and enforce customer-data boundaries appropriate to the architecture. They verify that access paths and material changes preserve those boundaries, and record the test method, result, and follow-up.
202
+
197
203
  ## Configuration Management and System Maintenance Policy
198
204
 
199
205
  Important Systems and Components use documented secure configuration expectations based on trusted guidance, technical capability, and risk. Owners change or disable unnecessary default accounts, credentials, services, ports, features, and configurations. Deviations require review and, when material, an approved Exception.
@@ -230,6 +236,8 @@ Important Systems record and protect the security and operational events needed
230
236
 
231
237
  Logs use synchronized time, restrict alteration and access, and avoid unnecessary secrets or personal data. Each System's retention period belongs in the approved Data Retention Schedule. Owners document risk-based alerts, review paths, thresholds, and response ownership in approved standards, procedures, and schedules.
232
238
 
239
+ When an incident may require investigation, responders preserve relevant logs and their context under the incident process, with controlled access and a documented retention or hold decision.
240
+
233
241
  ### Monitoring and alert testing
234
242
 
235
243
  Systems with availability commitments, recovery objectives, or material operational dependencies monitor the health, capacity, failure, and service indicators needed to detect degradation. Representative alert paths are tested from generation through acknowledgement, escalation, and fallback on the approved schedule and after a material path change. This requirement does not prescribe a particular monitoring or log-management product.
@@ -146,6 +146,7 @@ const DOCUMENT_CONTENT_UPDATES = [
146
146
  }
147
147
  ];
148
148
  const PRIOR_STARTER_POLICY_REVISIONS = new Set([
149
+ "b440eb17797624c1b456f778b12a955c871de308349e565b5d69ab6a25703673",
149
150
  "b0f9b988a8bd231fe70ce71b6a732970e709b7af7826c1b55f1532f511b6e511",
150
151
  "e87961c6a665d73e9d3cce96ab978df87579668e72ea236248680db1f69a5fa1",
151
152
  "2b50c75f4f138eebc7154a6b6e0e50b843f016c228632f7c71239ec27c18cbbf",
@@ -157,6 +158,16 @@ const PRIOR_STARTER_POLICY_REVISIONS = new Set([
157
158
  ]);
158
159
 
159
160
  const CONTROL_UPDATES = [
161
+ {
162
+ id: "control-access-review-offboarding",
163
+ prior: [{
164
+ activity: "Review access populations, record decisions, and remove dormant, expired, or unneeded access."
165
+ }],
166
+ next: {
167
+ activity: "Review complete access populations, including workers and third parties with customer-data access, record decisions, and remove dormant, expired, or unneeded access."
168
+ },
169
+ summary: "Prompt a complete customer-data access population without changing the approved review cadence."
170
+ },
160
171
  {
161
172
  id: "control-policy-management",
162
173
  prior: [{
@@ -283,15 +294,21 @@ const CONTROL_UPDATES = [
283
294
  },
284
295
  {
285
296
  id: "control-network-security",
286
- prior: [{
287
- statement: "The organization restricts network paths, protects remote access with approved encryption and authentication, and reviews material network access rules at least annually.",
288
- activity: "Manage boundaries, firewall rules, wireless safeguards, and remote production access."
289
- }],
297
+ prior: [
298
+ {
299
+ statement: "The organization restricts network paths, protects remote access with approved encryption and authentication, and reviews material network access rules at least annually.",
300
+ activity: "Manage boundaries, firewall rules, wireless safeguards, and remote production access."
301
+ },
302
+ {
303
+ statement: "The organization restricts network paths, separates production and nonproduction environments according to data and risk, protects remote access with approved encryption and authentication, and reviews material network access rules at least annually.",
304
+ activity: "Manage boundaries, environment connections, firewall rules, wireless safeguards, and remote production access."
305
+ }
306
+ ],
290
307
  next: {
291
308
  statement: "The organization restricts network paths, separates production and nonproduction environments according to data and risk, protects remote access with approved encryption and authentication, and reviews material network access rules at least annually.",
292
- activity: "Manage boundaries, environment connections, firewall rules, wireless safeguards, and remote production access."
309
+ activity: "Document and verify customer and environment boundaries, approved network rules and deviations by source, wireless safeguards, and remote production access."
293
310
  },
294
- summary: "Add risk-based production and nonproduction environment separation."
311
+ summary: "Prompt evidence for customer and environment boundaries and network-rule coverage."
295
312
  },
296
313
  {
297
314
  id: "control-change-management",
@@ -340,23 +357,32 @@ const CONTROL_UPDATES = [
340
357
  {
341
358
  activity: "Use continuous platform protection where supported and verify endpoint configuration, update, and compliance state on the risk-based schedule recorded in an Obligation when periodic work is needed.",
342
359
  requirementIds: ["requirement-soc2-cc6-6", "requirement-soc2-cc6-8", "requirement-soc2-cc7-1"]
360
+ },
361
+ {
362
+ activity: "Use continuous platform protection where supported and verify endpoint configuration, update, and compliance state on the approved risk-based schedule when periodic work is needed.",
363
+ requirementIds: ["requirement-soc2-cc6-6", "requirement-soc2-cc6-8", "requirement-soc2-cc7-1"]
343
364
  }
344
365
  ],
345
366
  next: {
346
- activity: "Use continuous platform protection where supported and verify endpoint configuration, update, and compliance state on the approved risk-based schedule when periodic work is needed.",
367
+ activity: "Document protection coverage and approved deviations by device or platform class. Use continuous platform protection where supported and verify configuration, updates, and compliance on the approved risk-based schedule when periodic work is needed.",
347
368
  requirementIds: ["requirement-soc2-cc6-6", "requirement-soc2-cc6-8", "requirement-soc2-cc7-1"]
348
369
  },
349
- summary: "Map endpoint malware protection to CC6.8 and keep implementation scheduling out of the Control description."
370
+ summary: "Prompt platform protection coverage, deviations, and verification without mandating one product."
350
371
  },
351
372
  {
352
373
  id: "control-vulnerability-management",
353
- prior: [{
354
- activity: "Choose scan coverage and cadence. Review the starter remediation targets of Critical 7 days, High 14 days, Medium 30 days, and Low 90 days, then record the approved targets or time-bound Exceptions."
355
- }],
374
+ prior: [
375
+ {
376
+ activity: "Choose scan coverage and cadence. Review the starter remediation targets of Critical 7 days, High 14 days, Medium 30 days, and Low 90 days, then record the approved targets or time-bound Exceptions."
377
+ },
378
+ {
379
+ activity: "Choose scan coverage and cadence, define approved risk-based remediation targets, and document time-bound Exceptions when a target cannot be met."
380
+ }
381
+ ],
356
382
  next: {
357
- activity: "Choose scan coverage and cadence, define approved risk-based remediation targets, and document time-bound Exceptions when a target cannot be met."
383
+ activity: "Define scan scope and cadence, severity criteria, risk-based remediation and patch targets, and time-bound Exceptions when a target cannot be met."
358
384
  },
359
- summary: "Keep starter-selection instructions and unapproved remediation targets out of the Control description."
385
+ summary: "Prompt explicit scan scope, severity criteria, and approved remediation and patch targets."
360
386
  },
361
387
  {
362
388
  id: "control-logging-monitoring",
@@ -403,6 +429,16 @@ const CONTROL_UPDATES = [
403
429
  activity: "Assess service, data, access, assurance, recovery, incidents, dependencies, supplied Components, and applicable contract safeguards before access or material reliance."
404
430
  },
405
431
  summary: "Name the contract outcomes considered when a Vendor handles protected data or supports an important service."
432
+ },
433
+ {
434
+ id: "control-vendor-monitoring",
435
+ prior: [{
436
+ activity: "Review vendor performance, assurance, recovery, access, incidents, and contract obligations."
437
+ }],
438
+ next: {
439
+ activity: "Set review intervals by Vendor risk and customer-data access. Review performance, assurance, recovery, access, incidents, and contract obligations, and reassess after material change."
440
+ },
441
+ summary: "Prompt review intervals for every customer-data Vendor while keeping the interval risk-based."
406
442
  }
407
443
  ];
408
444
 
@@ -439,6 +475,16 @@ const OBLIGATION_UPDATES = [
439
475
  }
440
476
  ];
441
477
 
478
+ const PRIOR_STARTER_RULE_RATIONALE = "Starter proposal derived from the linked Policy. Management must review the cadence, population, completion criteria, and timing before activation.";
479
+ const OBLIGATION_RULE_RATIONALE_UPDATES = [
480
+ ["obligation-rule-quarterly-privileged-access-review-v1", "Cover privileged and production access. Add other customer-data access only when an approved commitment or risk decision requires quarterly review."],
481
+ ["obligation-rule-annual-access-review-v1", "Cover other important access, including customer-data paths not assigned to a shorter approved review schedule. Avoid counting the same access twice."],
482
+ ["obligation-rule-monthly-endpoint-protection-verification-v1", "Confirm device and platform classes, protection coverage, exceptions, evidence, and whether the monthly cadence fits the approved risk decision."],
483
+ ["obligation-rule-annual-network-access-review-v1", "Confirm the in-scope network and host rule sources, customer and environment boundaries, deviations, and review cadence."],
484
+ ["obligation-rule-quarterly-vulnerability-scan-v1", "Confirm scan scope, severity method, remediation and patch targets, and whether exposure or a customer commitment requires a shorter cadence, such as monthly."],
485
+ ["obligation-rule-annual-critical-vendor-review-v1", "Confirm the high and critical Vendor population, and set a separate review interval for other Vendors with customer-data access where needed."]
486
+ ].map(([id, guidance]) => ({ id, rationale: `${PRIOR_STARTER_RULE_RATIONALE} ${guidance}` }));
487
+
442
488
  const OBLIGATION_ADDITIONS = [
443
489
  {
444
490
  id: "obligation-annual-control-design-review",
@@ -760,6 +806,37 @@ async function buildPolicyLibraryPlan(loaded) {
760
806
  });
761
807
  }
762
808
 
809
+ for (const { id, rationale } of OBLIGATION_RULE_RATIONALE_UPDATES) {
810
+ const entry = byId.get(id);
811
+ const obligation = entry ? byId.get(entry.record.obligationId)?.record : null;
812
+ const displayPath = `data/obligation-rules/${id}.json`;
813
+ if (!entry || entry.record.type !== "obligation-rule" || !obligation || obligation.type !== "obligation") {
814
+ skipped.push(skippedItem(id, "missing", `The starter ${id} Obligation Rule is not present.`));
815
+ continue;
816
+ }
817
+ if (entry.record.rationale === rationale) {
818
+ skipped.push(skippedItem(id, "current", "The Obligation Rule already contains the current starter guidance."));
819
+ continue;
820
+ }
821
+ if (entry.record.status !== "proposed" || obligation.status !== "proposed") {
822
+ skipped.push(skippedItem(id, "operating", "Only proposed Obligations and Rules are eligible for starter-library guidance updates."));
823
+ continue;
824
+ }
825
+ if (entry.record.rationale !== PRIOR_STARTER_RULE_RATIONALE || !obligation.ruleIds?.includes(id)) {
826
+ skipped.push(skippedItem(id, "customized", "The Obligation Rule differs from a recognized prior starter, so FileGRC will not rewrite it."));
827
+ continue;
828
+ }
829
+ updates.push({ ...entry.record, rationale });
830
+ expectedRevisions[id] = entry.revision;
831
+ proposalChanges.push({
832
+ resourceType: "obligation-rule",
833
+ resourceId: id,
834
+ path: displayPath,
835
+ summary: "Add scope and cadence checks to the unchanged proposed starter rule.",
836
+ diff: replacementDiff(displayPath, [["rationale", entry.record.rationale, rationale]])
837
+ });
838
+ }
839
+
763
840
  for (const addition of modelSupports(loaded.model, "program-scope") ? OBLIGATION_ADDITIONS : []) {
764
841
  if (byId.has(addition.id)) {
765
842
  skipped.push(skippedItem(addition.id, "present", "An Obligation with this ID already exists, so FileGRC will not replace it."));
@@ -17,7 +17,7 @@ export const RESOURCE_INSTRUCTIONS = {
17
17
  "reporting-route-set": "Replace the reporting-route placeholders with real normal and fallback channels, name the responsible role, and commit the proposal.",
18
18
  policy: "Replace Policy placeholders with actual commitments, then have a separate approver approve the exact Markdown revision.",
19
19
  document: "Complete the Document Markdown and intended values, assign an owner, and have a separate approver approve the exact revision.",
20
- control: "Open this Control’s current next steps. Set up the activity, record how it works, and mark it Implemented when it works.",
20
+ control: "Open this Control’s next steps and inspect linked Policy, Obligations, Components, and operating records. Reuse existing workflows; fill only the implementation gap, and mark it Implemented when it works. Record actual operation separately.",
21
21
  "complementary-control": "Record each real customer or carved-out provider dependency, or confirm that the current scope has none.",
22
22
  evidence: "Record a real artifact or approved reference, link its source and supported work, then have another person verify it.",
23
23
  "risk-assessment": "Assess the in-scope service, record conclusions, and obtain an independent review.",
@@ -118,7 +118,7 @@ export const RESOURCE_PAGE_SUMMARIES = {
118
118
  "retention-schedule-item": "Review each structured retention rule.",
119
119
  policy: "Tailor the starter Policy and have someone other than its owner approve it.",
120
120
  document: "Adapt and approve plans.",
121
- control: "Put each Control in place and record how it works.",
121
+ control: "Check existing Policy, obligations, and records, then fill each Control’s implementation gap.",
122
122
  "complementary-control": "Record customer or provider responsibilities, or confirm there are none.",
123
123
  audit: "Record the CPA engagement and scope.",
124
124
  "audit-request": "Track fieldwork requests and responses.",
@@ -2038,41 +2038,41 @@ function controlCheckLabel(name) {
2038
2038
  })[name] || name;
2039
2039
  }
2040
2040
 
2041
- const starterControlSetup = {
2042
- "control-security-governance": "Name a reviewer outside day-to-day security ownership and set up a quarterly security review.",
2043
- "control-policy-management": "Assign someone to maintain the Control list and a separate approver for policies and plans. Set up their review path.",
2044
- "control-security-communication": "Set up how the approved reporting routes, policy changes, and security notices reach staff and outside parties.",
2045
- "control-workforce-expectations": "Set up the checks, agreements, and policy acknowledgements required before someone receives sensitive access.",
2046
- "control-security-training": "Choose how new and existing workers receive security training, acknowledge the current material, and get reminders.",
2047
- "control-risk-assessment": "Use the Program's risk method to assess the scoped service and record the results in FileGRC Risk Assessments and Risks.",
2048
- "control-monitoring-remediation": "Define how Control problems are reported, reviewed, and verified closed. Use FileGRC Findings for gaps that need separate follow-up.",
2049
- "control-access-authorization": "Configure unique, least-privilege accounts in the identity system. Restrict account changes to authorized admins.",
2050
- "control-strong-authentication": "Turn on required MFA and secure sign-in settings for the in-scope Systems. Identify any gap that needs an approved Exception.",
2051
- "control-access-review-offboarding": "Confirm the identity system can produce a complete access list and admins can promptly remove or change permissions.",
2052
- "control-physical-workspace-security": "Decide how visitors, work areas, devices, and paper records are protected in the places your team actually works.",
2053
- "control-data-classification-inventory": "Apply the approved Classifications to important data in the in-scope Systems and record its location in the inventory.",
2054
- "control-encryption-transmission": "Configure encryption for in-scope data and devices, and assign who manages keys and approved transfer methods.",
2055
- "control-data-retention-disposal": "Apply the approved retention schedule to live, backup, and vendor-held data. Define how each class is deleted or destroyed.",
2056
- "control-inventory-configuration": "List important assets and owners, choose their secure settings, and decide how unsupported assets are handled.",
2057
- "control-endpoint-protection": "Apply the required device settings, updates, encryption, screen lock, and malware protection to devices that access company Systems.",
2058
- "control-network-security": "Restrict production network paths and remote access, and document the rules that allow traffic between environments.",
2059
- "control-change-management": "Set up a change path with risk review, testing, approval, deployment, and rollback for software and infrastructure changes.",
2060
- "control-vulnerability-management": "Choose what will be scanned, how often, who handles findings, and the target times for fixing them.",
2061
- "control-penetration-testing": "Decide whether independent testing is needed for this service. If it is, set its scope, cadence, and owner.",
2062
- "control-logging-monitoring": "Enable useful security logs and alerts for important Systems, protect them, and choose who responds to alerts.",
2063
- "control-incident-response": "Set up a way to report, assign, escalate, contain, and close security incidents using the approved response plan.",
2064
- "control-incident-exercise": "Choose a realistic incident scenario and an alert path to test, and assign the people who will run the exercise.",
2065
- "control-backup-restoration": "Choose what needs backup or another recovery path, configure it, and decide how restores will be tested.",
2066
- "control-continuity-exercise": "Set recovery priorities, contacts, and responsibilities, and choose how the continuity plan will be exercised.",
2067
- "control-vendor-due-diligence": "Set a security review and contract check before a new Vendor gets sensitive data or becomes a material dependency.",
2068
- "control-vendor-monitoring": "Choose which Vendors need ongoing review, who reviews them, and what information the review uses.",
2069
- "control-security-exceptions": "Document how an Exception is reviewed and approved before a departure from policy begins. Record actual decisions in FileGRC Exceptions."
2041
+ const starterControlChecks = {
2042
+ "control-security-governance": "Open the security review calendar and attendee roles. Check a reviewer separate from owners and operators examines risks, incidents, findings, and overdue work on the approved schedule.",
2043
+ "control-policy-management": "Open the Control inventory and a recently revised Policy. Check owners are named, the approver is separate, and the change went through review before approval.",
2044
+ "control-security-communication": "Trace how a security notice or Policy change reaches staff and affected outside parties. Check that the approved reporting routes can be used.",
2045
+ "control-workforce-expectations": "Review onboarding for roles with sensitive access. Check that required screening, agreements, and Policy acknowledgements happen before access.",
2046
+ "control-security-training": "Compare the worker roster with training assignments. Check starters and role changes get training on time, with repeats tied to current content and the approved schedule.",
2047
+ "control-risk-assessment": "Open the risk assessment against in-scope Systems and Vendors. Check threats, fraud, dependencies, and changes have owners and high risks get the required review.",
2048
+ "control-monitoring-remediation": "Open the latest Control review, or schedule the first. Check incidents, test results, Exceptions, and overdue work, with owners and verified closure for gaps.",
2049
+ "control-access-authorization": "Open access lists for in-scope Systems and Vendor tools. Check that accounts use unique identities and only needed permissions, and new grants require a business reason and approval.",
2050
+ "control-strong-authentication": "Open the System and Vendor lists. For each tool used for production, source code, email, identity, or sensitive data, check MFA, unique accounts, default credentials, and admin roles.",
2051
+ "control-access-review-offboarding": "Export access lists from key Systems and Vendor tools. Check privileged and other access reviews meet their approved schedules, and departures lose access within the required window.",
2052
+ "control-physical-workspace-security": "Walk through actual work areas and visitor access. Check who can enter nonpublic spaces and how devices, papers, screens, and conversations are protected.",
2053
+ "control-data-classification-inventory": "List important data stores in Systems and Vendor tools. Check each has an owner, classification, approved purpose, and recorded location.",
2054
+ "control-encryption-transmission": "For Systems, Vendor tools, and devices holding Confidential or Restricted data, check encryption at rest and in transit and who can manage the keys.",
2055
+ "control-data-retention-disposal": "Take each class in the retention schedule and locate its live, backup, and vendor-held copies. Check deletion methods and timing against the approved rule.",
2056
+ "control-inventory-configuration": "Compare actual services, devices, software, service accounts, Vendors, and data stores with the inventory. Check owners, secure settings, and unsupported assets.",
2057
+ "control-endpoint-protection": "Review devices allowed into company Systems. Check encryption, screen lock, updates, and malware protection against the approved settings.",
2058
+ "control-network-security": "Open production network and remote-access rules. Check each path has a business reason, approved authentication, and separation; identify rules due for review.",
2059
+ "control-change-management": "Follow a software or infrastructure change from request to deployment. Check the path includes risk review, testing, approval, and a way to recover.",
2060
+ "control-vulnerability-management": "List internet-facing and important Systems. Check each has vulnerability scan coverage or another approved check, and findings have owners and target dates.",
2061
+ "control-penetration-testing": "Review the service's exposure, major changes, and customer commitments. Decide whether independent testing is needed and, if so, set its scope and cadence.",
2062
+ "control-logging-monitoring": "For each important System, open security logs, alert rules, and service-health checks where needed. Check retention and whether test alerts reach an owner.",
2063
+ "control-incident-response": "Walk through how a worker reports an incident. Check who triages, escalates, contains, communicates, and closes it using the approved plan.",
2064
+ "control-incident-exercise": "Send a test alert through a representative incident path. Check acknowledgement, escalation, and fallback, then record participants, failures, and follow-up.",
2065
+ "control-backup-restoration": "For each important System, inspect backup or alternate recovery settings and failure alerts. Run or review a restore test against its recovery needs.",
2066
+ "control-continuity-exercise": "Review recovery priorities, contacts, and owners. Check the plan covers loss of an important System or provider and has an exercise date.",
2067
+ "control-vendor-due-diligence": "Open the Vendor list. For each with sensitive data or a material service role, check its security review and contract terms; review older Vendors already in use.",
2068
+ "control-vendor-monitoring": "Open critical and high-risk Vendors. Check each has a current review, an owner for open issues, and timely reassessment after a material change or incident.",
2069
+ "control-security-exceptions": "Review departures from security rules. Check each has a reason, owner, risk assessment, compensating measure, approval, and expiry or review date."
2070
2070
  };
2071
2071
 
2072
2072
  export function controlImplementationSteps(control, checks, options = {}) {
2073
2073
  const steps = [];
2074
- if (!checks.implemented) steps.push(starterControlSetup[control.id]
2075
- || `Set up this Control: ${control.activity || control.statement}`);
2074
+ if (!checks.implemented) steps.push(starterControlChecks[control.id]
2075
+ || "Inspect the systems, people, vendors, or work this Control covers. Compare current practice with its statement and fix any gaps.");
2076
2076
  const record = [];
2077
2077
  if (!checks.owner) record.push("choose an owner");
2078
2078
  if (!checks.scope) record.push("select the Systems this covers");
@@ -2092,7 +2092,7 @@ export function controlImplementationSteps(control, checks, options = {}) {
2092
2092
  ? `Cover the missing evidence families: ${sourceTargets.length ? sourceTargets.join(" and ") : "this Control"}. For each, link an active Component with the matching kind, evidence owners, and report retrieval instructions.`
2093
2093
  : null;
2094
2094
  const queueStep = checks.workQueue === false
2095
- ? "Enable a linked Obligation with the owner and schedule for this Control's recurring or event work."
2095
+ ? "Check the linked Obligations. Enable one with the owner and schedule for this Control's recurring or event work, or add one if none covers it."
2096
2096
  : null;
2097
2097
  if (!checks.implemented) {
2098
2098
  if (sourceStep || queueStep) steps.push([sourceStep, queueStep].filter(Boolean).join(" "));
package/src/server.js CHANGED
@@ -670,8 +670,9 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
670
670
  : await createResourceDetail(input, type, id, { includeHistory: url.searchParams.get("history") !== "false" });
671
671
  if (!entry) return json(response, 404, { error: "Resource not found." });
672
672
  if (includeWorkflow && !session) {
673
- const workflow = await assessWorkflow(input, { programId: requestOptions.programId });
674
- entry.workflow = workflowForResource(workflow, type, id);
673
+ const loaded = await loadWorkspace(input);
674
+ const workflow = await assessWorkflow(loaded, { programId: requestOptions.programId });
675
+ entry.workflow = workflowForResource(workflow, type, id, loaded);
675
676
  }
676
677
  return json(response, 200, entry);
677
678
  }
@@ -1008,7 +1009,7 @@ async function loadStateSessionResource(session, token, type, id, serverOptions,
1008
1009
  historyDeadlineAt: deadlineAt,
1009
1010
  strictHistory: repository?.git?.available === true
1010
1011
  }));
1011
- detail.workflow = workflowForResource(workflow, type, id);
1012
+ detail.workflow = workflowForResource(workflow, type, id, session.loaded);
1012
1013
  }
1013
1014
  await verifyStateSessionSnapshot(session, performance.now(), deadlineAt);
1014
1015
  assertCurrentStateSession(session);
package/src/workflow.js CHANGED
@@ -2,6 +2,7 @@ import { cp, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { modelSupports } from "../model/index.js";
5
+ import { buildActionContext } from "./action-context.js";
5
6
  import { applicabilityReviewIsCurrent, applicabilityScopeRevision } from "./applicability-scope.js";
6
7
  import { retentionScheduleIsAuthoritative } from "./collection-review.js";
7
8
  import { assessRequiredAppointments } from "./appointments.js";
@@ -194,7 +195,7 @@ async function assessWorkflowUnmeasured(input, options = {}) {
194
195
  findings: findings.sort(compareFindings),
195
196
  workItems: workItems.sort(compareWorkItems),
196
197
  recommended: recommended
197
- ? { ...recommended, rankingReason: rankingReason(recommended) }
198
+ ? { ...recommended, rankingReason: rankingReason(recommended), context: buildActionContext(loaded, recommended) }
198
199
  : null,
199
200
  counts: {
200
201
  findings: countBy(findings, "state"),
@@ -219,7 +220,7 @@ export function buildWorkflowDelta(before, after) {
219
220
  };
220
221
  }
221
222
 
222
- export function workflowForResource(workflow, type, id) {
223
+ export function workflowForResource(workflow, type, id, loaded = null) {
223
224
  if (!workflow) return {
224
225
  findings: [],
225
226
  workItems: [],
@@ -246,12 +247,16 @@ export function workflowForResource(workflow, type, id) {
246
247
  related: {
247
248
  findings: relatedFindings,
248
249
  workItems: relatedWorkItems,
249
- recommended: [...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0] || null
250
+ recommended: contextualAction([...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0], loaded)
250
251
  },
251
- recommended: [...findings, ...workItems].sort(compareRecommended)[0] || null
252
+ recommended: contextualAction([...findings, ...workItems].sort(compareRecommended)[0], loaded)
252
253
  };
253
254
  }
254
255
 
256
+ function contextualAction(item, loaded) {
257
+ return item && loaded ? { ...item, context: buildActionContext(loaded, item) } : item || null;
258
+ }
259
+
255
260
  export async function previewWorkflowMutation(input, mutation) {
256
261
  const loaded = await loadWorkspace(input);
257
262
  const previewRoot = await mkdtemp(join(tmpdir(), "filegrc-workflow-preview-"));
@@ -1129,6 +1134,9 @@ function normalizeFinding(code, item, context) {
1129
1134
  title: item.title,
1130
1135
  message: item.message,
1131
1136
  ...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
1137
+ ...(item.checks ? { unmetChecks: Object.entries(item.checks)
1138
+ .filter(([, passed]) => passed === false)
1139
+ .map(([name]) => name) } : {}),
1132
1140
  ...(item.resourceId ? { resourceId: item.resourceId } : {}),
1133
1141
  ...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
1134
1142
  ...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),