@greenarmor/ges 1.3.0 → 1.4.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/dist/cli.js +2 -0
- package/dist/commands/audit.js +28 -19
- package/dist/commands/doctor.js +48 -7
- package/dist/commands/governance.d.ts +2 -0
- package/dist/commands/governance.js +726 -0
- package/dist/commands/init.js +35 -27
- package/dist/commands/policy.js +14 -10
- package/dist/commands/score.js +6 -2
- package/dist/utils/next-steps.js +11 -6
- package/dist/utils/prompts.d.ts +8 -0
- package/dist/utils/prompts.js +62 -11
- package/dist/utils/ui.d.ts +37 -0
- package/dist/utils/ui.js +115 -0
- package/package.json +18 -14
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureGESInitialized } from "../utils/project.js";
|
|
3
|
+
import { input, select } from "../utils/prompts.js";
|
|
4
|
+
import { banner, divider, blank, success, info, kv, statusBadge, severityBadge, BOLD, DIM, GREEN, RED, YELLOW, GRAY, } from "../utils/ui.js";
|
|
5
|
+
import { loadGovernanceRecords, createGovernanceRecord, addGovernanceRecord, findGovernanceRecord, setGovernanceApproval, addGovernanceEvidence, createEvidenceRef, verifyGovernanceRecord, deleteGovernanceRecord, setGovernanceRiskAssessment, setGovernancePolicyBasis, setGovernanceReviewCycle, setGovernanceDataInventory, setGovernanceComplianceLinks, setGovernanceCommittee, } from "@greenarmor/ges-core";
|
|
6
|
+
import { recordActivity } from "@greenarmor/ges-core";
|
|
7
|
+
const STATUS_BADGE = {
|
|
8
|
+
draft: "○",
|
|
9
|
+
"pending-review": "◐",
|
|
10
|
+
approved: "●",
|
|
11
|
+
rejected: "✕",
|
|
12
|
+
conditional: "◔",
|
|
13
|
+
expired: "⚠",
|
|
14
|
+
revoked: "✕",
|
|
15
|
+
};
|
|
16
|
+
const RISK_COLOR = {
|
|
17
|
+
low: "LOW",
|
|
18
|
+
medium: "MEDIUM",
|
|
19
|
+
high: "HIGH",
|
|
20
|
+
critical: "CRITICAL",
|
|
21
|
+
};
|
|
22
|
+
function printRecordSummary(record) {
|
|
23
|
+
console.log(` ${statusBadge(record.status)} ${BOLD(record.system_name)}`);
|
|
24
|
+
console.log(` ${DIM("ID")} ${record.id}`);
|
|
25
|
+
console.log(` ${DIM("Type")} ${record.system_type} ${GRAY("|")} ${DIM("Risk")} ${severityBadge(record.risk_level)}`);
|
|
26
|
+
if (record.approval) {
|
|
27
|
+
console.log(` ${DIM("By")} ${record.approval.approver_name} (${record.approval.approver_role})`);
|
|
28
|
+
console.log(` ${DIM("Valid")} ${record.approval.valid_from} ${GRAY("→")} ${record.approval.valid_until || "indefinite"}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(` ${DIM("By")} ${GRAY("NOT RECORDED")}`);
|
|
32
|
+
}
|
|
33
|
+
console.log(` ${DIM("Ev")} ${record.evidence.length} reference(s)`);
|
|
34
|
+
}
|
|
35
|
+
export const governanceCommand = new Command("governance")
|
|
36
|
+
.description("Manage governance approval provenance chains")
|
|
37
|
+
.addCommand(new Command("add")
|
|
38
|
+
.description("Create a new governance record")
|
|
39
|
+
.option("-n, --name <name>", "System name")
|
|
40
|
+
.option("--type <type>", "System type")
|
|
41
|
+
.option("--risk <level>", "Risk level (low/medium/high/critical)")
|
|
42
|
+
.option("--desc <description>", "System description")
|
|
43
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
44
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
45
|
+
.action(async (options) => {
|
|
46
|
+
const name = options.name || await input({ message: "System name:", default: "" });
|
|
47
|
+
if (!name) {
|
|
48
|
+
console.error(" Error: System name is required.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const systemType = (options.type || await select({
|
|
52
|
+
message: "System type:",
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: "AI System", value: "ai-system" },
|
|
55
|
+
{ name: "Application", value: "application" },
|
|
56
|
+
{ name: "Data Process", value: "data-process" },
|
|
57
|
+
{ name: "API", value: "api" },
|
|
58
|
+
{ name: "Model", value: "model" },
|
|
59
|
+
{ name: "Infrastructure", value: "infrastructure" },
|
|
60
|
+
{ name: "Third-Party Service", value: "third-party-service" },
|
|
61
|
+
],
|
|
62
|
+
}));
|
|
63
|
+
const riskLevel = (options.risk || await select({
|
|
64
|
+
message: "Risk level:",
|
|
65
|
+
choices: [
|
|
66
|
+
{ name: "Low", value: "low" },
|
|
67
|
+
{ name: "Medium", value: "medium" },
|
|
68
|
+
{ name: "High", value: "high" },
|
|
69
|
+
{ name: "Critical", value: "critical" },
|
|
70
|
+
],
|
|
71
|
+
}));
|
|
72
|
+
const description = options.desc || await input({ message: "System description:", default: "" });
|
|
73
|
+
const root = ensureGESInitialized();
|
|
74
|
+
const record = createGovernanceRecord({
|
|
75
|
+
system_name: name,
|
|
76
|
+
system_description: description,
|
|
77
|
+
system_type: systemType,
|
|
78
|
+
risk_level: riskLevel,
|
|
79
|
+
created_by: "cli-user",
|
|
80
|
+
});
|
|
81
|
+
addGovernanceRecord(root, record);
|
|
82
|
+
blank();
|
|
83
|
+
success("Governance record created");
|
|
84
|
+
kv("ID", record.id, 6);
|
|
85
|
+
console.log();
|
|
86
|
+
printRecordSummary(record);
|
|
87
|
+
console.log(`\n ${DIM("Next steps:")}`);
|
|
88
|
+
console.log(` ${GRAY("–")} ${GREEN("ges governance approve")} ${record.id} ${DIM("Record approval decision")}`);
|
|
89
|
+
console.log(` ${GRAY("–")} ${GREEN("ges governance evidence")} ${record.id} ${DIM("Add evidence reference")}`);
|
|
90
|
+
console.log(` ${GRAY("–")} ${GREEN("ges governance verify")} ${record.id} ${DIM("Verify provenance chain")}\n`);
|
|
91
|
+
recordActivity(root, {
|
|
92
|
+
source: "cli",
|
|
93
|
+
action: "control_override",
|
|
94
|
+
title: `Governance record created: ${name}`,
|
|
95
|
+
description: `Created governance record for ${name} (${systemType}, risk: ${riskLevel}). Record ID: ${record.id}`,
|
|
96
|
+
details: { governance_record_id: record.id, system_type: systemType, risk_level: riskLevel },
|
|
97
|
+
actor_name: options.actor,
|
|
98
|
+
actor_role: options.actorRole,
|
|
99
|
+
});
|
|
100
|
+
}))
|
|
101
|
+
.addCommand(new Command("approve")
|
|
102
|
+
.description("Record an approval decision for a governance record")
|
|
103
|
+
.argument("<id>", "Record ID or system name")
|
|
104
|
+
.option("--approver <name>", "Approver name")
|
|
105
|
+
.option("--role <role>", "Approver role")
|
|
106
|
+
.option("--email <email>", "Approver email")
|
|
107
|
+
.option("--authority <authority>", "Approval authority")
|
|
108
|
+
.option("--decision <decision>", "Decision: approved, rejected, conditional")
|
|
109
|
+
.option("--valid-from <date>", "Validity start date (YYYY-MM-DD)")
|
|
110
|
+
.option("--valid-until <date>", "Validity end date (ISO 8601)")
|
|
111
|
+
.option("--conditions <conditions>", "Conditions (comma-separated)")
|
|
112
|
+
.option("--rationale <text>", "Rationale for the decision")
|
|
113
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
114
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
115
|
+
.action(async (id, options) => {
|
|
116
|
+
const root = ensureGESInitialized();
|
|
117
|
+
const record = findGovernanceRecord(root, id);
|
|
118
|
+
if (!record) {
|
|
119
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const approverName = options.approver || await input({ message: "Approver name:", default: "" });
|
|
123
|
+
const approverRole = options.role || await input({ message: "Approver role:", default: "" });
|
|
124
|
+
const approverEmail = options.email || await input({ message: "Approver email:", default: "" });
|
|
125
|
+
const authority = options.authority || await input({ message: "Approval authority:", default: "" });
|
|
126
|
+
const decision = (options.decision || await select({
|
|
127
|
+
message: "Decision:",
|
|
128
|
+
choices: [
|
|
129
|
+
{ name: "Approved", value: "approved" },
|
|
130
|
+
{ name: "Conditional", value: "conditional" },
|
|
131
|
+
{ name: "Rejected", value: "rejected" },
|
|
132
|
+
],
|
|
133
|
+
}));
|
|
134
|
+
const validFrom = options.validFrom || new Date().toISOString().split("T")[0];
|
|
135
|
+
const validUntil = options.validUntil || await input({ message: "Valid until (YYYY-MM-DD, or blank for indefinite):", default: "" });
|
|
136
|
+
const conditionsStr = options.conditions || await input({ message: "Conditions (comma-separated):", default: "" });
|
|
137
|
+
const rationale = options.rationale || await input({ message: "Rationale:", default: "" });
|
|
138
|
+
const updated = setGovernanceApproval(root, record.id, {
|
|
139
|
+
approver_name: approverName,
|
|
140
|
+
approver_role: approverRole,
|
|
141
|
+
approver_email: approverEmail,
|
|
142
|
+
approval_authority: authority,
|
|
143
|
+
decision,
|
|
144
|
+
decision_date: new Date().toISOString(),
|
|
145
|
+
valid_from: validFrom,
|
|
146
|
+
valid_until: validUntil || null,
|
|
147
|
+
conditions: conditionsStr ? conditionsStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
148
|
+
rationale,
|
|
149
|
+
}, "cli-user");
|
|
150
|
+
if (!updated) {
|
|
151
|
+
console.error(` Error: Failed to update record.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
blank();
|
|
155
|
+
success("Approval recorded", `for ${updated.system_name}`);
|
|
156
|
+
kv("Decision", decision.toUpperCase(), 6);
|
|
157
|
+
kv("Approver", `${approverName} (${approverRole})`, 6);
|
|
158
|
+
kv("Valid", `${validFrom} → ${validUntil || "indefinite"}`, 6);
|
|
159
|
+
console.log();
|
|
160
|
+
recordActivity(root, {
|
|
161
|
+
source: "cli",
|
|
162
|
+
action: "control_override",
|
|
163
|
+
title: `Governance approval: ${updated.system_name} → ${decision}`,
|
|
164
|
+
description: `${approverName} (${approverRole}) marked ${updated.system_name} as ${decision}. Valid until: ${validUntil || "indefinite"}.`,
|
|
165
|
+
details: { governance_record_id: updated.id, decision },
|
|
166
|
+
actor_name: options.actor,
|
|
167
|
+
actor_role: options.actorRole,
|
|
168
|
+
});
|
|
169
|
+
}))
|
|
170
|
+
.addCommand(new Command("evidence")
|
|
171
|
+
.description("Add an evidence reference to a governance record")
|
|
172
|
+
.argument("<id>", "Record ID or system name")
|
|
173
|
+
.option("--title <title>", "Evidence title")
|
|
174
|
+
.option("--source <system>", "Source system (jira, confluence, servicenow, etc.)")
|
|
175
|
+
.option("--reference <ref>", "Reference (ticket ID, URL, document name)")
|
|
176
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
177
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
178
|
+
.action(async (id, options) => {
|
|
179
|
+
const root = ensureGESInitialized();
|
|
180
|
+
const record = findGovernanceRecord(root, id);
|
|
181
|
+
if (!record) {
|
|
182
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const title = options.title || await input({ message: "Evidence title:", default: "" });
|
|
186
|
+
const sourceSystem = (options.source || await select({
|
|
187
|
+
message: "Source system:",
|
|
188
|
+
choices: [
|
|
189
|
+
{ name: "Jira", value: "jira" },
|
|
190
|
+
{ name: "Confluence", value: "confluence" },
|
|
191
|
+
{ name: "ServiceNow", value: "servicenow" },
|
|
192
|
+
{ name: "SharePoint", value: "sharepoint" },
|
|
193
|
+
{ name: "GRC Platform", value: "grc-platform" },
|
|
194
|
+
{ name: "Git", value: "git" },
|
|
195
|
+
{ name: "File", value: "file" },
|
|
196
|
+
{ name: "URL", value: "url" },
|
|
197
|
+
{ name: "Email", value: "email" },
|
|
198
|
+
{ name: "Other", value: "other" },
|
|
199
|
+
],
|
|
200
|
+
}));
|
|
201
|
+
const reference = options.reference || await input({ message: "Reference (ticket ID, URL, doc name):", default: "" });
|
|
202
|
+
const evidenceType = await select({
|
|
203
|
+
message: "Evidence type:",
|
|
204
|
+
choices: [
|
|
205
|
+
{ name: "Document", value: "document" },
|
|
206
|
+
{ name: "Ticket", value: "ticket" },
|
|
207
|
+
{ name: "Meeting Record", value: "meeting-record" },
|
|
208
|
+
{ name: "Report", value: "report" },
|
|
209
|
+
{ name: "Certificate", value: "certificate" },
|
|
210
|
+
{ name: "Contract", value: "contract" },
|
|
211
|
+
{ name: "Log", value: "log" },
|
|
212
|
+
{ name: "Dashboard", value: "dashboard" },
|
|
213
|
+
{ name: "Email", value: "email" },
|
|
214
|
+
{ name: "Other", value: "other" },
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
const locationDesc = await input({ message: "Location description:", default: "" });
|
|
218
|
+
const evidence = createEvidenceRef({
|
|
219
|
+
type: evidenceType,
|
|
220
|
+
title,
|
|
221
|
+
source_system: sourceSystem,
|
|
222
|
+
reference,
|
|
223
|
+
location_description: locationDesc,
|
|
224
|
+
added_by: "cli-user",
|
|
225
|
+
});
|
|
226
|
+
const updated = addGovernanceEvidence(root, record.id, evidence, "cli-user");
|
|
227
|
+
if (!updated) {
|
|
228
|
+
console.error(` Error: Failed to add evidence.`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
blank();
|
|
232
|
+
success("Evidence added", `to ${updated.system_name}`);
|
|
233
|
+
console.log(` ${BOLD(evidence.title)}`);
|
|
234
|
+
kv("Source", evidence.source_system, 6);
|
|
235
|
+
kv("Ref", evidence.reference, 6);
|
|
236
|
+
kv("Total", `${updated.evidence.length} reference(s)`, 6);
|
|
237
|
+
console.log();
|
|
238
|
+
recordActivity(root, {
|
|
239
|
+
source: "cli",
|
|
240
|
+
action: "control_override",
|
|
241
|
+
title: `Evidence added: ${evidence.title}`,
|
|
242
|
+
description: `Added evidence reference "${evidence.title}" (${evidence.source_system}: ${evidence.reference}) to governance record ${updated.system_name}.`,
|
|
243
|
+
details: { governance_record_id: updated.id, evidence_id: evidence.id, source: evidence.source_system },
|
|
244
|
+
actor_name: options.actor,
|
|
245
|
+
actor_role: options.actorRole,
|
|
246
|
+
});
|
|
247
|
+
}))
|
|
248
|
+
.addCommand(new Command("list")
|
|
249
|
+
.description("List all governance records")
|
|
250
|
+
.action(() => {
|
|
251
|
+
const root = ensureGESInitialized();
|
|
252
|
+
const records = loadGovernanceRecords(root);
|
|
253
|
+
if (records.length === 0) {
|
|
254
|
+
info("No governance records found.");
|
|
255
|
+
console.log(` ${DIM("Create one with:")} ${GREEN("ges governance add")}\n`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
blank();
|
|
259
|
+
console.log(` ${BOLD("Governance Records")} ${GRAY(`(${records.length})`)}`);
|
|
260
|
+
console.log();
|
|
261
|
+
records.forEach(r => {
|
|
262
|
+
printRecordSummary(r);
|
|
263
|
+
console.log();
|
|
264
|
+
});
|
|
265
|
+
}))
|
|
266
|
+
.addCommand(new Command("show")
|
|
267
|
+
.description("Show full provenance chain for a governance record")
|
|
268
|
+
.argument("<id>", "Record ID or system name")
|
|
269
|
+
.action((id) => {
|
|
270
|
+
const root = ensureGESInitialized();
|
|
271
|
+
const record = findGovernanceRecord(root, id);
|
|
272
|
+
if (!record) {
|
|
273
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
console.log(`\n ═══════════════════════════════════════════════════`);
|
|
277
|
+
console.log(` GOVERNANCE RECORD: ${record.system_name}`);
|
|
278
|
+
console.log(` ═══════════════════════════════════════════════════\n`);
|
|
279
|
+
console.log(` SYSTEM IDENTITY`);
|
|
280
|
+
console.log(` ID: ${record.id}`);
|
|
281
|
+
console.log(` Name: ${record.system_name}`);
|
|
282
|
+
console.log(` Description: ${record.system_description || "(none)"}`);
|
|
283
|
+
console.log(` Type: ${record.system_type}`);
|
|
284
|
+
console.log(` Version: ${record.system_version || "(none)"}`);
|
|
285
|
+
console.log(` Status: ${record.status}`);
|
|
286
|
+
console.log(` Risk Level: ${record.risk_level}\n`);
|
|
287
|
+
console.log(` RISK ASSESSMENT`);
|
|
288
|
+
if (record.risk_assessment) {
|
|
289
|
+
const ra = record.risk_assessment;
|
|
290
|
+
console.log(` Assessor: ${ra.assessor}`);
|
|
291
|
+
console.log(` Date: ${ra.assessment_date}`);
|
|
292
|
+
console.log(` Methodology: ${ra.methodology}`);
|
|
293
|
+
console.log(` Risk Score: ${ra.risk_score}`);
|
|
294
|
+
console.log(` Residual Risk: ${ra.residual_risk}`);
|
|
295
|
+
if (ra.identified_risks.length)
|
|
296
|
+
console.log(` Identified: ${ra.identified_risks.join(", ")}`);
|
|
297
|
+
if (ra.mitigation_measures.length)
|
|
298
|
+
console.log(` Mitigations: ${ra.mitigation_measures.join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
console.log(` ⚠ NOT RECORDED`);
|
|
302
|
+
}
|
|
303
|
+
console.log("");
|
|
304
|
+
console.log(` POLICY BASIS`);
|
|
305
|
+
if (record.policy_basis) {
|
|
306
|
+
const pb = record.policy_basis;
|
|
307
|
+
console.log(` Policy ID: ${pb.policy_id}`);
|
|
308
|
+
console.log(` Name: ${pb.policy_name}`);
|
|
309
|
+
console.log(` Version: ${pb.version}`);
|
|
310
|
+
console.log(` Standard: ${pb.standard}`);
|
|
311
|
+
if (pb.clauses.length)
|
|
312
|
+
console.log(` Clauses: ${pb.clauses.join(", ")}`);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
console.log(` ⚠ NOT RECORDED`);
|
|
316
|
+
}
|
|
317
|
+
console.log("");
|
|
318
|
+
console.log(` APPROVAL DECISION`);
|
|
319
|
+
if (record.approval) {
|
|
320
|
+
const a = record.approval;
|
|
321
|
+
console.log(` Approver: ${a.approver_name} (${a.approver_role})`);
|
|
322
|
+
console.log(` Email: ${a.approver_email || "(none)"}`);
|
|
323
|
+
console.log(` Authority: ${a.approval_authority}`);
|
|
324
|
+
console.log(` Decision: ${a.decision.toUpperCase()}`);
|
|
325
|
+
console.log(` Date: ${a.decision_date}`);
|
|
326
|
+
console.log(` Valid From: ${a.valid_from}`);
|
|
327
|
+
console.log(` Valid Until: ${a.valid_until || "indefinite"}`);
|
|
328
|
+
if (a.conditions.length)
|
|
329
|
+
console.log(` Conditions: ${a.conditions.join("; ")}`);
|
|
330
|
+
if (a.rationale)
|
|
331
|
+
console.log(` Rationale: ${a.rationale}`);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
console.log(` ⚠ NOT RECORDED`);
|
|
335
|
+
}
|
|
336
|
+
console.log("");
|
|
337
|
+
console.log(` COMMITTEE APPROVAL`);
|
|
338
|
+
if (record.committee) {
|
|
339
|
+
const c = record.committee;
|
|
340
|
+
console.log(` Committee: ${c.committee_name}`);
|
|
341
|
+
console.log(` Meeting: ${c.meeting_date} (${c.meeting_reference})`);
|
|
342
|
+
if (c.attendees.length)
|
|
343
|
+
console.log(` Attendees: ${c.attendees.join(", ")}`);
|
|
344
|
+
console.log(` Summary: ${c.decision_summary}`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
console.log(` (not required or not recorded)`);
|
|
348
|
+
}
|
|
349
|
+
console.log("");
|
|
350
|
+
console.log(` EVIDENCE CHAIN (${record.evidence.length})`);
|
|
351
|
+
if (record.evidence.length === 0) {
|
|
352
|
+
console.log(` ⚠ NO EVIDENCE REFERENCES`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
record.evidence.forEach((e, i) => {
|
|
356
|
+
console.log(` [${i + 1}] ${e.title}`);
|
|
357
|
+
console.log(` Type: ${e.type} | Source: ${e.source_system}`);
|
|
358
|
+
console.log(` Ref: ${e.reference}`);
|
|
359
|
+
console.log(` Loc: ${e.location_description}`);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
console.log("");
|
|
363
|
+
console.log(` REVIEW CYCLE`);
|
|
364
|
+
if (record.review_cycle) {
|
|
365
|
+
const rc = record.review_cycle;
|
|
366
|
+
console.log(` Frequency: ${rc.frequency}`);
|
|
367
|
+
console.log(` Last Review: ${rc.last_review}`);
|
|
368
|
+
console.log(` Next Review: ${rc.next_review}`);
|
|
369
|
+
if (rc.review_history.length) {
|
|
370
|
+
console.log(` History:`);
|
|
371
|
+
rc.review_history.forEach(h => {
|
|
372
|
+
console.log(` ${h.date} — ${h.outcome} (${h.reviewer}): ${h.notes}`);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.log(` ⚠ NOT DEFINED — continuous compliance not monitored`);
|
|
378
|
+
}
|
|
379
|
+
console.log("");
|
|
380
|
+
console.log(` DATA INVENTORY`);
|
|
381
|
+
if (record.data_inventory) {
|
|
382
|
+
const di = record.data_inventory;
|
|
383
|
+
if (di.personal_data_categories.length)
|
|
384
|
+
console.log(` Data Categories: ${di.personal_data_categories.join(", ")}`);
|
|
385
|
+
if (di.processing_purposes.length)
|
|
386
|
+
console.log(` Purposes: ${di.processing_purposes.join(", ")}`);
|
|
387
|
+
if (di.data_subjects.length)
|
|
388
|
+
console.log(` Data Subjects: ${di.data_subjects.join(", ")}`);
|
|
389
|
+
if (di.cross_border_transfers.length)
|
|
390
|
+
console.log(` Transfers: ${di.cross_border_transfers.join(", ")}`);
|
|
391
|
+
console.log(` Retention: ${di.retention_period}`);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
console.log(` (not documented)`);
|
|
395
|
+
}
|
|
396
|
+
console.log("");
|
|
397
|
+
console.log(` COMPLIANCE LINKS`);
|
|
398
|
+
if (record.compliance) {
|
|
399
|
+
const cl = record.compliance;
|
|
400
|
+
if (cl.frameworks.length)
|
|
401
|
+
console.log(` Frameworks: ${cl.frameworks.join(", ")}`);
|
|
402
|
+
if (cl.controls_satisfied.length)
|
|
403
|
+
console.log(` Controls: ${cl.controls_satisfied.join(", ")}`);
|
|
404
|
+
if (cl.control_pack_ids.length)
|
|
405
|
+
console.log(` Control Packs: ${cl.control_pack_ids.join(", ")}`);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
console.log(` (not mapped)`);
|
|
409
|
+
}
|
|
410
|
+
console.log(`\n Created: ${record.created_at} by ${record.created_by}`);
|
|
411
|
+
console.log(` Updated: ${record.updated_at} by ${record.updated_by} (v${record.record_version})\n`);
|
|
412
|
+
}))
|
|
413
|
+
.addCommand(new Command("verify")
|
|
414
|
+
.description("Verify the provenance chain completeness of a governance record")
|
|
415
|
+
.argument("<id>", "Record ID or system name")
|
|
416
|
+
.action((id) => {
|
|
417
|
+
const root = ensureGESInitialized();
|
|
418
|
+
const record = findGovernanceRecord(root, id);
|
|
419
|
+
if (!record) {
|
|
420
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const result = verifyGovernanceRecord(record);
|
|
424
|
+
banner("VERIFICATION", record.system_name);
|
|
425
|
+
const overallText = result.valid ? GREEN(BOLD("✓ VALID")) : RED(BOLD("✕ ISSUES FOUND"));
|
|
426
|
+
console.log(` ${DIM("Overall:")} ${overallText}`);
|
|
427
|
+
console.log(` ${DIM("Approval:")} ${statusBadge(result.approval_status)}`);
|
|
428
|
+
if (result.days_until_expiry !== null) {
|
|
429
|
+
const dayLabel = result.days_until_expiry < 0
|
|
430
|
+
? RED(`${Math.abs(result.days_until_expiry)} days AGO`)
|
|
431
|
+
: result.days_until_expiry <= 30
|
|
432
|
+
? YELLOW(`${result.days_until_expiry} days remaining`)
|
|
433
|
+
: GREEN(`${result.days_until_expiry} days remaining`);
|
|
434
|
+
console.log(` ${DIM("Expiry:")} ${dayLabel}`);
|
|
435
|
+
}
|
|
436
|
+
console.log(` ${DIM("Evidence:")} ${result.completeness.evidence_count} reference(s)`);
|
|
437
|
+
console.log(`\n ${BOLD("Completeness Checklist")}`);
|
|
438
|
+
divider(40);
|
|
439
|
+
const check = (ok, label, isWarning = false) => {
|
|
440
|
+
const icon = ok ? GREEN("✓") : isWarning ? YELLOW("△") : RED("✕");
|
|
441
|
+
const text = ok ? label : isWarning ? YELLOW(label) : RED(label);
|
|
442
|
+
console.log(` ${icon} ${text}`);
|
|
443
|
+
};
|
|
444
|
+
check(result.completeness.has_approval, "Approval Decision");
|
|
445
|
+
check(result.completeness.has_risk_assessment, "Risk Assessment");
|
|
446
|
+
check(result.completeness.has_policy_basis, "Policy Basis");
|
|
447
|
+
check(result.completeness.has_evidence, "Evidence Chain");
|
|
448
|
+
check(result.completeness.has_review_cycle, "Review Cycle", true);
|
|
449
|
+
check(result.completeness.has_data_inventory, "Data Inventory", true);
|
|
450
|
+
check(result.completeness.has_compliance_links, "Compliance Links", true);
|
|
451
|
+
check(result.completeness.is_current, "Currently Valid");
|
|
452
|
+
if (result.issues.length > 0) {
|
|
453
|
+
console.log(`\n ${RED(BOLD("BLOCKING ISSUES"))}`);
|
|
454
|
+
result.issues.forEach(i => console.log(` ${RED("✕")} ${i}`));
|
|
455
|
+
}
|
|
456
|
+
if (result.warnings.length > 0) {
|
|
457
|
+
console.log(`\n ${YELLOW(BOLD("WARNINGS"))}`);
|
|
458
|
+
result.warnings.forEach(w => console.log(` ${YELLOW("△")} ${w}`));
|
|
459
|
+
}
|
|
460
|
+
console.log();
|
|
461
|
+
}))
|
|
462
|
+
.addCommand(new Command("delete")
|
|
463
|
+
.description("Delete a governance record")
|
|
464
|
+
.argument("<id>", "Record ID or system name")
|
|
465
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
466
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
467
|
+
.action((id, options) => {
|
|
468
|
+
const root = ensureGESInitialized();
|
|
469
|
+
const record = findGovernanceRecord(root, id);
|
|
470
|
+
if (!record) {
|
|
471
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
const deleted = deleteGovernanceRecord(root, record.id);
|
|
475
|
+
if (deleted) {
|
|
476
|
+
blank();
|
|
477
|
+
success("Deleted governance record", `${record.system_name} (${record.id})`);
|
|
478
|
+
console.log();
|
|
479
|
+
recordActivity(root, {
|
|
480
|
+
source: "cli",
|
|
481
|
+
action: "control_override",
|
|
482
|
+
title: `Governance record deleted: ${record.system_name}`,
|
|
483
|
+
description: `Deleted governance record ${record.system_name} (${record.id}).`,
|
|
484
|
+
details: { governance_record_id: record.id },
|
|
485
|
+
actor_name: options.actor,
|
|
486
|
+
actor_role: options.actorRole,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
console.error(` Error: Failed to delete record.`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
}))
|
|
494
|
+
.addCommand(new Command("risk-assessment")
|
|
495
|
+
.description("Link a risk assessment to a governance record")
|
|
496
|
+
.argument("<id>", "Record ID or system name")
|
|
497
|
+
.option("--assessor <name>", "Risk assessor name")
|
|
498
|
+
.option("--methodology <text>", "Assessment methodology")
|
|
499
|
+
.option("--score <score>", "Risk score (e.g., 7.5/10)")
|
|
500
|
+
.option("--residual <level>", "Residual risk level")
|
|
501
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
502
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
503
|
+
.action(async (id, options) => {
|
|
504
|
+
const root = ensureGESInitialized();
|
|
505
|
+
const record = findGovernanceRecord(root, id);
|
|
506
|
+
if (!record) {
|
|
507
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
const assessor = options.assessor || await input({ message: "Assessor name:", default: "" });
|
|
511
|
+
const methodology = options.methodology || await input({ message: "Methodology:", default: "" });
|
|
512
|
+
const score = options.score || await input({ message: "Risk score:", default: "" });
|
|
513
|
+
const residual = options.residual || await input({ message: "Residual risk level:", default: "" });
|
|
514
|
+
const risksStr = await input({ message: "Identified risks (comma-separated):", default: "" });
|
|
515
|
+
const mitigationsStr = await input({ message: "Mitigation measures (comma-separated):", default: "" });
|
|
516
|
+
const updated = setGovernanceRiskAssessment(root, record.id, {
|
|
517
|
+
id: `risk-${Date.now()}`,
|
|
518
|
+
assessor,
|
|
519
|
+
assessment_date: new Date().toISOString(),
|
|
520
|
+
methodology,
|
|
521
|
+
risk_score: score,
|
|
522
|
+
identified_risks: risksStr ? risksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
523
|
+
residual_risk: residual,
|
|
524
|
+
mitigation_measures: mitigationsStr ? mitigationsStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
525
|
+
evidence: [],
|
|
526
|
+
}, "cli-user");
|
|
527
|
+
if (!updated) {
|
|
528
|
+
console.error(` Error: Failed to update.`);
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
blank();
|
|
532
|
+
success("Risk assessment linked", `to ${updated.system_name}`);
|
|
533
|
+
kv("Assessor", assessor, 6);
|
|
534
|
+
kv("Score", score, 6);
|
|
535
|
+
kv("Residual", residual, 6);
|
|
536
|
+
console.log();
|
|
537
|
+
recordActivity(root, { source: "cli", action: "control_override", title: `Risk assessment added: ${updated.system_name}`, description: `Risk assessment by ${assessor} linked to ${updated.system_name}. Score: ${score}, Residual: ${residual}.`, details: { governance_record_id: updated.id }, actor_name: options.actor, actor_role: options.actorRole });
|
|
538
|
+
}))
|
|
539
|
+
.addCommand(new Command("policy-basis")
|
|
540
|
+
.description("Document the policy basis for a governance record")
|
|
541
|
+
.argument("<id>", "Record ID or system name")
|
|
542
|
+
.option("--policy-id <id>", "Policy ID")
|
|
543
|
+
.option("--policy-name <name>", "Policy name")
|
|
544
|
+
.option("--pv <version>", "Policy version")
|
|
545
|
+
.option("--standard <std>", "Standard (e.g., GDPR, ISO 27001)")
|
|
546
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
547
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
548
|
+
.action(async (id, options) => {
|
|
549
|
+
const root = ensureGESInitialized();
|
|
550
|
+
const record = findGovernanceRecord(root, id);
|
|
551
|
+
if (!record) {
|
|
552
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
const policyId = options.policyId || await input({ message: "Policy ID:", default: "" });
|
|
556
|
+
const policyName = options.policyName || await input({ message: "Policy name:", default: "" });
|
|
557
|
+
const version = options.pv || await input({ message: "Policy version:", default: "1.0" });
|
|
558
|
+
const standard = options.standard || await input({ message: "Standard:", default: "" });
|
|
559
|
+
const clausesStr = await input({ message: "Applicable clauses (comma-separated):", default: "" });
|
|
560
|
+
const updated = setGovernancePolicyBasis(root, record.id, {
|
|
561
|
+
policy_id: policyId,
|
|
562
|
+
policy_name: policyName,
|
|
563
|
+
version,
|
|
564
|
+
clauses: clausesStr ? clausesStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
565
|
+
standard,
|
|
566
|
+
evidence: [],
|
|
567
|
+
}, "cli-user");
|
|
568
|
+
if (!updated) {
|
|
569
|
+
console.error(` Error: Failed to update.`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
blank();
|
|
573
|
+
success("Policy basis documented", `for ${updated.system_name}`);
|
|
574
|
+
kv("Policy", `${policyName} (${policyId} v${version})`, 6);
|
|
575
|
+
kv("Standard", standard, 6);
|
|
576
|
+
console.log();
|
|
577
|
+
recordActivity(root, { source: "cli", action: "control_override", title: `Policy basis added: ${updated.system_name}`, description: `Policy ${policyName} (${policyId} v${version}) documented for ${updated.system_name}.`, details: { governance_record_id: updated.id }, actor_name: options.actor, actor_role: options.actorRole });
|
|
578
|
+
}))
|
|
579
|
+
.addCommand(new Command("review-cycle")
|
|
580
|
+
.description("Set up a review cycle for a governance record")
|
|
581
|
+
.argument("<id>", "Record ID or system name")
|
|
582
|
+
.option("--frequency <freq>", "Frequency: quarterly, semi-annual, annual, biennial")
|
|
583
|
+
.option("--next-review <date>", "Next review date (YYYY-MM-DD)")
|
|
584
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
585
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
586
|
+
.action(async (id, options) => {
|
|
587
|
+
const root = ensureGESInitialized();
|
|
588
|
+
const record = findGovernanceRecord(root, id);
|
|
589
|
+
if (!record) {
|
|
590
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
const frequency = (options.frequency || await select({
|
|
594
|
+
message: "Review frequency:",
|
|
595
|
+
choices: [
|
|
596
|
+
{ name: "Quarterly", value: "quarterly" },
|
|
597
|
+
{ name: "Semi-Annual", value: "semi-annual" },
|
|
598
|
+
{ name: "Annual", value: "annual" },
|
|
599
|
+
{ name: "Biennial", value: "biennial" },
|
|
600
|
+
],
|
|
601
|
+
}));
|
|
602
|
+
const today = new Date().toISOString().split("T")[0];
|
|
603
|
+
const nextReview = options.nextReview || await input({ message: "Next review date (YYYY-MM-DD):", default: today });
|
|
604
|
+
const updated = setGovernanceReviewCycle(root, record.id, {
|
|
605
|
+
frequency,
|
|
606
|
+
last_review: today,
|
|
607
|
+
next_review: nextReview,
|
|
608
|
+
review_history: [],
|
|
609
|
+
}, "cli-user");
|
|
610
|
+
if (!updated) {
|
|
611
|
+
console.error(` Error: Failed to update.`);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
blank();
|
|
615
|
+
success("Review cycle set", `for ${updated.system_name}`);
|
|
616
|
+
kv("Frequency", frequency, 6);
|
|
617
|
+
kv("Next review", nextReview, 6);
|
|
618
|
+
console.log();
|
|
619
|
+
recordActivity(root, { source: "cli", action: "control_override", title: `Review cycle set: ${updated.system_name}`, description: `Review cycle (${frequency}) set for ${updated.system_name}. Next review: ${nextReview}.`, details: { governance_record_id: updated.id }, actor_name: options.actor, actor_role: options.actorRole });
|
|
620
|
+
}))
|
|
621
|
+
.addCommand(new Command("data-inventory")
|
|
622
|
+
.description("Document the data inventory for a governance record")
|
|
623
|
+
.argument("<id>", "Record ID or system name")
|
|
624
|
+
.option("--categories <cats>", "Personal data categories (comma-separated)")
|
|
625
|
+
.option("--purposes <purp>", "Processing purposes (comma-separated)")
|
|
626
|
+
.option("--retention <period>", "Retention period")
|
|
627
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
628
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
629
|
+
.action(async (id, options) => {
|
|
630
|
+
const root = ensureGESInitialized();
|
|
631
|
+
const record = findGovernanceRecord(root, id);
|
|
632
|
+
if (!record) {
|
|
633
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
const categoriesStr = options.categories || await input({ message: "Personal data categories (comma-separated):", default: "" });
|
|
637
|
+
const purposesStr = options.purposes || await input({ message: "Processing purposes (comma-separated):", default: "" });
|
|
638
|
+
const subjectsStr = await input({ message: "Data subjects (comma-separated):", default: "" });
|
|
639
|
+
const transfersStr = await input({ message: "Cross-border transfers (comma-separated):", default: "" });
|
|
640
|
+
const retention = options.retention || await input({ message: "Retention period:", default: "" });
|
|
641
|
+
const updated = setGovernanceDataInventory(root, record.id, {
|
|
642
|
+
personal_data_categories: categoriesStr ? categoriesStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
643
|
+
processing_purposes: purposesStr ? purposesStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
644
|
+
data_subjects: subjectsStr ? subjectsStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
645
|
+
cross_border_transfers: transfersStr ? transfersStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
646
|
+
retention_period: retention,
|
|
647
|
+
}, "cli-user");
|
|
648
|
+
if (!updated) {
|
|
649
|
+
console.error(` Error: Failed to update.`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
blank();
|
|
653
|
+
success("Data inventory documented", `for ${updated.system_name}`);
|
|
654
|
+
console.log();
|
|
655
|
+
recordActivity(root, { source: "cli", action: "control_override", title: `Data inventory added: ${updated.system_name}`, description: `Data inventory documented for ${updated.system_name}.`, details: { governance_record_id: updated.id }, actor_name: options.actor, actor_role: options.actorRole });
|
|
656
|
+
}))
|
|
657
|
+
.addCommand(new Command("committee")
|
|
658
|
+
.description("Record committee approval for a governance record")
|
|
659
|
+
.argument("<id>", "Record ID or system name")
|
|
660
|
+
.option("--committee <name>", "Committee name")
|
|
661
|
+
.option("--meeting-ref <ref>", "Meeting reference")
|
|
662
|
+
.option("--meeting-date <date>", "Meeting date (YYYY-MM-DD)")
|
|
663
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
664
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
665
|
+
.action(async (id, options) => {
|
|
666
|
+
const root = ensureGESInitialized();
|
|
667
|
+
const record = findGovernanceRecord(root, id);
|
|
668
|
+
if (!record) {
|
|
669
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
const committeeName = options.committee || await input({ message: "Committee name:", default: "" });
|
|
673
|
+
const meetingRef = options.meetingRef || await input({ message: "Meeting reference:", default: "" });
|
|
674
|
+
const meetingDate = options.meetingDate || await input({ message: "Meeting date (YYYY-MM-DD):", default: "" });
|
|
675
|
+
const attendeesStr = await input({ message: "Attendees (comma-separated):", default: "" });
|
|
676
|
+
const summary = await input({ message: "Decision summary:", default: "" });
|
|
677
|
+
const updated = setGovernanceCommittee(root, record.id, {
|
|
678
|
+
committee_name: committeeName,
|
|
679
|
+
meeting_date: meetingDate,
|
|
680
|
+
meeting_reference: meetingRef,
|
|
681
|
+
attendees: attendeesStr ? attendeesStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
682
|
+
decision_summary: summary,
|
|
683
|
+
evidence: [],
|
|
684
|
+
}, "cli-user");
|
|
685
|
+
if (!updated) {
|
|
686
|
+
console.error(` Error: Failed to update.`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
blank();
|
|
690
|
+
success("Committee approval recorded", `for ${updated.system_name}`);
|
|
691
|
+
kv("Committee", committeeName, 6);
|
|
692
|
+
kv("Meeting", `${meetingDate} (${meetingRef})`, 6);
|
|
693
|
+
console.log();
|
|
694
|
+
recordActivity(root, { source: "cli", action: "control_override", title: `Committee approval added: ${updated.system_name}`, description: `Committee ${committeeName} (${meetingRef}) recorded for ${updated.system_name}.`, details: { governance_record_id: updated.id }, actor_name: options.actor, actor_role: options.actorRole });
|
|
695
|
+
}))
|
|
696
|
+
.addCommand(new Command("compliance-links")
|
|
697
|
+
.description("Map compliance frameworks to a governance record")
|
|
698
|
+
.argument("<id>", "Record ID or system name")
|
|
699
|
+
.option("--frameworks <fw>", "Frameworks (comma-separated, e.g., GDPR,OWASP)")
|
|
700
|
+
.option("--controls <ctrls>", "Controls satisfied (comma-separated)")
|
|
701
|
+
.option("--actor <name>", "Name of person performing this action")
|
|
702
|
+
.option("--actor-role <role>", "Role of person performing this action")
|
|
703
|
+
.action(async (id, options) => {
|
|
704
|
+
const root = ensureGESInitialized();
|
|
705
|
+
const record = findGovernanceRecord(root, id);
|
|
706
|
+
if (!record) {
|
|
707
|
+
console.error(` Error: Governance record "${id}" not found.`);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const frameworksStr = options.frameworks || await input({ message: "Frameworks (comma-separated):", default: "" });
|
|
711
|
+
const controlsStr = options.controls || await input({ message: "Controls satisfied (comma-separated):", default: "" });
|
|
712
|
+
const packsStr = await input({ message: "Control pack IDs (comma-separated):", default: "" });
|
|
713
|
+
const updated = setGovernanceComplianceLinks(root, record.id, {
|
|
714
|
+
frameworks: frameworksStr ? frameworksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
715
|
+
controls_satisfied: controlsStr ? controlsStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
716
|
+
control_pack_ids: packsStr ? packsStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
717
|
+
}, "cli-user");
|
|
718
|
+
if (!updated) {
|
|
719
|
+
console.error(` Error: Failed to update.`);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
blank();
|
|
723
|
+
success("Compliance links mapped", `for ${updated.system_name}`);
|
|
724
|
+
console.log();
|
|
725
|
+
recordActivity(root, { source: "cli", action: "control_override", title: `Compliance links added: ${updated.system_name}`, description: `Compliance frameworks mapped for ${updated.system_name}.`, details: { governance_record_id: updated.id }, actor_name: options.actor, actor_role: options.actorRole });
|
|
726
|
+
}));
|