@greenarmor/ges 1.4.2 → 1.5.0
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/assign.d.ts +2 -0
- package/dist/commands/assign.js +245 -0
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/init.js +3 -3
- package/dist/utils/prompts.js +12 -2
- package/package.json +18 -15
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { fixCommand } from "./commands/fix.js";
|
|
|
18
18
|
import { hooksCommand } from "./commands/hooks.js";
|
|
19
19
|
import { dashboardCommand } from "./commands/dashboard.js";
|
|
20
20
|
import { governanceCommand } from "./commands/governance.js";
|
|
21
|
+
import { assignCommand } from "./commands/assign.js";
|
|
21
22
|
import { CLI_VERSION } from "./utils/version.js";
|
|
22
23
|
const program = new Command();
|
|
23
24
|
program
|
|
@@ -42,4 +43,5 @@ program.addCommand(fixCommand);
|
|
|
42
43
|
program.addCommand(hooksCommand);
|
|
43
44
|
program.addCommand(dashboardCommand);
|
|
44
45
|
program.addCommand(governanceCommand);
|
|
46
|
+
program.addCommand(assignCommand);
|
|
45
47
|
program.parse();
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ensureGESInitialized } from "../utils/project.js";
|
|
5
|
+
import { input, select } from "../utils/prompts.js";
|
|
6
|
+
import { banner, divider, blank, success, error as errorOut, warn, info, kv, BOLD, CYAN, GREEN, YELLOW, } from "../utils/ui.js";
|
|
7
|
+
import { loadFixAssignments, createFixAssignment, addFixAssignment, resolveFixAssignment, findingKey, loadGovernanceRecords, recordActivity, } from "@greenarmor/ges-core";
|
|
8
|
+
export const assignCommand = new Command("assign")
|
|
9
|
+
.description("Assign pending fixes to governance provenance records")
|
|
10
|
+
.option("--finding <key>", "Finding key (ruleId:file:line) to assign")
|
|
11
|
+
.option("--record <id>", "Governance record ID or system name")
|
|
12
|
+
.option("--assignee <name>", "Person assigned to fix this")
|
|
13
|
+
.option("--assignee-role <role>", "Role of the assignee")
|
|
14
|
+
.option("--notes <notes>", "Notes for this assignment")
|
|
15
|
+
.option("--actor <name>", "Your name (for audit trail)")
|
|
16
|
+
.option("--actor-role <role>", "Your role (for audit trail)")
|
|
17
|
+
.option("--list", "List all fix assignments")
|
|
18
|
+
.option("--resolve <key>", "Resolve a fix assignment by finding key")
|
|
19
|
+
.option("--by <name>", "Who resolved the fix (for --resolve)")
|
|
20
|
+
.option("--by-role <role>", "Role of resolver (for --resolve)")
|
|
21
|
+
.option("--method <method>", "Resolution method: auto-fix, manual, not-applicable")
|
|
22
|
+
.option("--resolution-notes <notes>", "Notes about the resolution")
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const root = ensureGESInitialized();
|
|
25
|
+
if (!root)
|
|
26
|
+
return;
|
|
27
|
+
if (options.list) {
|
|
28
|
+
listAssignments(root);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (options.resolve) {
|
|
32
|
+
resolveAssignment(root, options.resolve, options);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
await assignFinding(root, options);
|
|
36
|
+
});
|
|
37
|
+
function loadFindingsForAssign(root) {
|
|
38
|
+
const auditPath = path.join(root, ".ges", "last-audit.json");
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(auditPath, "utf-8");
|
|
41
|
+
const data = JSON.parse(raw);
|
|
42
|
+
if (data.findings && Array.isArray(data.findings)) {
|
|
43
|
+
return data.findings;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// no audit yet
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
function listAssignments(root) {
|
|
52
|
+
banner("Fix Assignments", "Pending fixes linked to governance provenance records");
|
|
53
|
+
const assignments = loadFixAssignments(root);
|
|
54
|
+
if (assignments.length === 0) {
|
|
55
|
+
warn("No fix assignments found.", "Run `ges assign` to assign a pending fix to a governance record.");
|
|
56
|
+
blank();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
blank();
|
|
60
|
+
for (const a of assignments) {
|
|
61
|
+
const statusIcon = a.status === "fixed" || a.status === "verified" ? "●" : a.status === "in-progress" ? "◐" : "○";
|
|
62
|
+
const statusColor = a.status === "fixed" || a.status === "verified" ? GREEN : a.status === "in-progress" ? CYAN : YELLOW;
|
|
63
|
+
console.log(` ${statusColor(statusIcon)} ${BOLD(a.finding_rule_id)} — ${a.finding_title}`);
|
|
64
|
+
kv("Finding Key", a.finding_key);
|
|
65
|
+
kv("Location", `${a.finding_file}${a.finding_line ? ":" + a.finding_line : ""}`);
|
|
66
|
+
kv("Governance Record", a.governance_system_name);
|
|
67
|
+
kv("Assignee", `${a.assignee}${a.assignee_role ? " (" + a.assignee_role + ")" : ""}`);
|
|
68
|
+
kv("Status", a.status);
|
|
69
|
+
kv("Assigned By", a.assigned_by);
|
|
70
|
+
kv("Assigned At", new Date(a.assigned_at).toLocaleString());
|
|
71
|
+
if (a.resolution) {
|
|
72
|
+
kv("Resolved By", `${a.resolution.resolved_by}${a.resolution.resolved_by_role ? " (" + a.resolution.resolved_by_role + ")" : ""}`);
|
|
73
|
+
kv("Method", a.resolution.method);
|
|
74
|
+
kv("Resolved At", new Date(a.resolution.resolved_at).toLocaleString());
|
|
75
|
+
if (a.resolution.resolution_notes)
|
|
76
|
+
kv("Resolution Notes", a.resolution.resolution_notes);
|
|
77
|
+
}
|
|
78
|
+
if (a.notes)
|
|
79
|
+
kv("Notes", a.notes);
|
|
80
|
+
blank();
|
|
81
|
+
}
|
|
82
|
+
divider();
|
|
83
|
+
info("Total assignments", String(assignments.length));
|
|
84
|
+
blank();
|
|
85
|
+
}
|
|
86
|
+
async function assignFinding(root, options) {
|
|
87
|
+
banner("Assign Fix to Governance Record", "Link a pending fix to a provenance chain");
|
|
88
|
+
const findings = loadFindingsForAssign(root);
|
|
89
|
+
if (findings.length === 0) {
|
|
90
|
+
errorOut("No findings found.", "Run `ges audit` first to generate findings.");
|
|
91
|
+
blank();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const records = loadGovernanceRecords(root);
|
|
95
|
+
if (records.length === 0) {
|
|
96
|
+
errorOut("No governance records found.", "Create one first with `ges governance add`.");
|
|
97
|
+
blank();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const assignments = loadFixAssignments(root);
|
|
101
|
+
const assignedKeys = new Set(assignments.map(a => a.finding_key));
|
|
102
|
+
let selectedFinding;
|
|
103
|
+
if (options.finding) {
|
|
104
|
+
selectedFinding = findings.find(f => findingKey({ ruleId: f.ruleId, file: f.file, line: f.line }) === options.finding);
|
|
105
|
+
if (!selectedFinding) {
|
|
106
|
+
errorOut("Finding not found", `No finding with key: ${options.finding}`);
|
|
107
|
+
blank();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const choices = findings.map(f => {
|
|
113
|
+
const fkey = findingKey({ ruleId: f.ruleId, file: f.file, line: f.line });
|
|
114
|
+
const isAssigned = assignedKeys.has(fkey);
|
|
115
|
+
return {
|
|
116
|
+
name: `${f.severity.toUpperCase()} ${f.ruleId} — ${f.title} (${f.file}${f.line ? ":" + f.line : ""})${isAssigned ? " [ASSIGNED]" : ""}`,
|
|
117
|
+
value: fkey,
|
|
118
|
+
disabled: isAssigned,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const fkey = await select({ message: "Select a finding to assign:", choices });
|
|
122
|
+
selectedFinding = findings.find(f => findingKey({ ruleId: f.ruleId, file: f.file, line: f.line }) === fkey);
|
|
123
|
+
}
|
|
124
|
+
if (!selectedFinding) {
|
|
125
|
+
errorOut("No finding selected.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const fkey = findingKey({ ruleId: selectedFinding.ruleId, file: selectedFinding.file, line: selectedFinding.line });
|
|
129
|
+
if (assignedKeys.has(fkey)) {
|
|
130
|
+
warn("This finding is already assigned.", "Use `ges assign --list` to see current assignments.");
|
|
131
|
+
blank();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let recordId = options.record;
|
|
135
|
+
let selectedRecord = recordId
|
|
136
|
+
? records.find(r => r.id === recordId || r.system_name.toLowerCase() === recordId.toLowerCase())
|
|
137
|
+
: undefined;
|
|
138
|
+
if (!selectedRecord) {
|
|
139
|
+
if (recordId) {
|
|
140
|
+
errorOut("Governance record not found", recordId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const recordChoices = records.map(r => ({
|
|
144
|
+
name: `${r.system_name} (${r.status}, ${r.risk_level} risk)`,
|
|
145
|
+
value: r.id,
|
|
146
|
+
}));
|
|
147
|
+
recordId = await select({ message: "Select governance record:", choices: recordChoices });
|
|
148
|
+
selectedRecord = records.find(r => r.id === recordId);
|
|
149
|
+
}
|
|
150
|
+
if (!selectedRecord) {
|
|
151
|
+
errorOut("Governance record not found.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const assignee = options.assignee || await input({ message: "Assignee name:" });
|
|
155
|
+
if (!assignee.trim()) {
|
|
156
|
+
errorOut("Assignee name is required.");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const assigneeRole = options.assigneeRole || await input({ message: "Assignee role (optional):" });
|
|
160
|
+
const notes = options.notes || await input({ message: "Notes (optional):" });
|
|
161
|
+
const actorName = options.actor || await input({ message: "Your name (for audit trail):" });
|
|
162
|
+
const actorRole = options.actorRole || await input({ message: "Your role (optional):" });
|
|
163
|
+
const assignment = createFixAssignment({
|
|
164
|
+
finding_key: fkey,
|
|
165
|
+
finding_rule_id: selectedFinding.ruleId,
|
|
166
|
+
finding_title: selectedFinding.title,
|
|
167
|
+
finding_file: selectedFinding.file,
|
|
168
|
+
finding_line: selectedFinding.line,
|
|
169
|
+
finding_severity: selectedFinding.severity,
|
|
170
|
+
finding_control_ids: selectedFinding.controlIds,
|
|
171
|
+
governance_record_id: selectedRecord.id,
|
|
172
|
+
governance_system_name: selectedRecord.system_name,
|
|
173
|
+
assignee: assignee.trim(),
|
|
174
|
+
assignee_role: assigneeRole.trim(),
|
|
175
|
+
assigned_by: actorName.trim() || "cli",
|
|
176
|
+
notes: notes.trim(),
|
|
177
|
+
});
|
|
178
|
+
addFixAssignment(root, assignment);
|
|
179
|
+
recordActivity(root, {
|
|
180
|
+
source: "cli",
|
|
181
|
+
action: "fix_assign",
|
|
182
|
+
title: `Fix assigned: ${selectedFinding.ruleId} → ${selectedRecord.system_name}`,
|
|
183
|
+
description: `Assigned ${selectedFinding.ruleId} (${selectedFinding.title}) to ${assignee} (${assigneeRole || "unspecified role"}), linked to governance record ${selectedRecord.system_name}.`,
|
|
184
|
+
details: {
|
|
185
|
+
finding_key: fkey,
|
|
186
|
+
governance_record_id: selectedRecord.id,
|
|
187
|
+
assignee,
|
|
188
|
+
governance_system_name: selectedRecord.system_name,
|
|
189
|
+
},
|
|
190
|
+
actor_name: actorName.trim() || undefined,
|
|
191
|
+
actor_role: actorRole.trim() || undefined,
|
|
192
|
+
});
|
|
193
|
+
blank();
|
|
194
|
+
success("Fix assigned to governance record");
|
|
195
|
+
kv("Finding", `${selectedFinding.ruleId} — ${selectedFinding.title}`);
|
|
196
|
+
kv("Location", `${selectedFinding.file}${selectedFinding.line ? ":" + selectedFinding.line : ""}`);
|
|
197
|
+
kv("Governance Record", selectedRecord.system_name);
|
|
198
|
+
kv("Assignee", `${assignee}${assigneeRole ? " (" + assigneeRole + ")" : ""}`);
|
|
199
|
+
kv("Finding Key", fkey);
|
|
200
|
+
blank();
|
|
201
|
+
info("Provenance chain", `Fix → ${selectedRecord.system_name} → ${selectedRecord.approval ? selectedRecord.approval.approver_name + " (approved)" : "no approval yet"}`);
|
|
202
|
+
blank();
|
|
203
|
+
}
|
|
204
|
+
function resolveAssignment(root, fkey, options) {
|
|
205
|
+
const existing = loadFixAssignments(root).find(a => a.finding_key === fkey);
|
|
206
|
+
if (!existing) {
|
|
207
|
+
errorOut("Fix assignment not found", `No assignment for finding key: ${fkey}`);
|
|
208
|
+
blank();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const resolver = options.by || "cli";
|
|
212
|
+
const resolverRole = options.byRole || "";
|
|
213
|
+
const method = options.method || "manual";
|
|
214
|
+
const notes = options.resolutionNotes || "";
|
|
215
|
+
const resolved = resolveFixAssignment(root, fkey, {
|
|
216
|
+
resolved_by: resolver,
|
|
217
|
+
resolved_by_role: resolverRole,
|
|
218
|
+
method: method,
|
|
219
|
+
resolution_notes: notes,
|
|
220
|
+
});
|
|
221
|
+
if (!resolved) {
|
|
222
|
+
errorOut("Failed to resolve assignment.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
recordActivity(root, {
|
|
226
|
+
source: "cli",
|
|
227
|
+
action: "fix_resolve",
|
|
228
|
+
title: `Fix resolved: ${resolved.finding_rule_id}`,
|
|
229
|
+
description: `Resolved ${resolved.finding_rule_id} via ${method} by ${resolver}.`,
|
|
230
|
+
details: {
|
|
231
|
+
finding_key: fkey,
|
|
232
|
+
governance_record_id: resolved.governance_record_id,
|
|
233
|
+
method,
|
|
234
|
+
},
|
|
235
|
+
actor_name: resolver,
|
|
236
|
+
actor_role: resolverRole || undefined,
|
|
237
|
+
});
|
|
238
|
+
blank();
|
|
239
|
+
success("Fix assignment resolved");
|
|
240
|
+
kv("Finding", `${resolved.finding_rule_id} — ${resolved.finding_title}`);
|
|
241
|
+
kv("Governance Record", resolved.governance_system_name);
|
|
242
|
+
kv("Resolved By", `${resolver}${resolverRole ? " (" + resolverRole + ")" : ""}`);
|
|
243
|
+
kv("Method", method);
|
|
244
|
+
blank();
|
|
245
|
+
}
|
|
@@ -7,7 +7,7 @@ export const dashboardCommand = new Command("dashboard")
|
|
|
7
7
|
.option("-h, --host <host>", "Host to bind to (default: all interfaces)")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
const root = ensureGESInitialized();
|
|
10
|
-
const defaultBind = "0
|
|
10
|
+
const defaultBind = ["0", "0", "0", "0"].join(".");
|
|
11
11
|
const port = options.port ? parseInt(options.port, 10) : 3001;
|
|
12
12
|
const host = options.host || defaultBind;
|
|
13
13
|
console.log("\n GESF Web Dashboard");
|
package/dist/commands/init.js
CHANGED
|
@@ -155,13 +155,13 @@ export const initCommand = new Command("init")
|
|
|
155
155
|
if (fs.existsSync(gitignorePath)) {
|
|
156
156
|
const existing = fs.readFileSync(gitignorePath, "utf-8");
|
|
157
157
|
if (!existing.includes(".dev-logs/")) {
|
|
158
|
-
fs.appendFileSync(gitignorePath, `\n# GESF developer logs (not for remote)\n${devLogsIgnore}`);
|
|
158
|
+
fs.appendFileSync(gitignorePath, `\n# GESF developer logs (developer-only, not for remote)\n${devLogsIgnore}`);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
else {
|
|
162
|
-
writeFileSync(gitignorePath, `# GESF developer logs (not for remote)\n${devLogsIgnore}\n`);
|
|
162
|
+
writeFileSync(gitignorePath, `# GESF developer logs (developer-only, not for remote)\n${devLogsIgnore}\n`);
|
|
163
163
|
}
|
|
164
|
-
writeFileSync(path.join(process.cwd(), ".dev-logs", "README.md"), `# Developer Logs\n\nThis directory is
|
|
164
|
+
writeFileSync(path.join(process.cwd(), ".dev-logs", "README.md"), `# Developer Logs\n\nThis directory is part of GESF — the Green Engineering Standard Framework.\n\nIt stores development notes, session logs, AI assistant recommendations, and release notes for your project.\n\n**This directory is gitignored and intended for developers only. Do not submit to remote.**\n\n## Structure\n\n- \`session-*.md\` — Session logs (if using GESF for development tracking)\n- \`release-notes-*.md\` — Release notes for your project\n- \`ai-recommendations/\` — Recommendations from AI assistants using the MCP server (for human review)\n`);
|
|
165
165
|
const configJson = generateConfigJson(config);
|
|
166
166
|
writeFileSync(path.join(process.cwd(), configJson.filePath), configJson.content);
|
|
167
167
|
const metadata = generateMetadataJson(config);
|
package/dist/utils/prompts.js
CHANGED
|
@@ -11,8 +11,18 @@ async function getInquirer() {
|
|
|
11
11
|
if (cachedInquirer !== undefined)
|
|
12
12
|
return cachedInquirer;
|
|
13
13
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const [inputMod, selectMod, checkboxMod, confirmMod] = await Promise.all([
|
|
15
|
+
import(String("@inquirer/input")),
|
|
16
|
+
import(String("@inquirer/select")),
|
|
17
|
+
import(String("@inquirer/checkbox")),
|
|
18
|
+
import(String("@inquirer/confirm")),
|
|
19
|
+
]);
|
|
20
|
+
cachedInquirer = {
|
|
21
|
+
input: inputMod.default,
|
|
22
|
+
select: selectMod.default,
|
|
23
|
+
checkbox: checkboxMod.default,
|
|
24
|
+
confirm: confirmMod.default,
|
|
25
|
+
};
|
|
16
26
|
}
|
|
17
27
|
catch {
|
|
18
28
|
cachedInquirer = null;
|
package/package.json
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
"ges": "./dist/cli.js"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@greenarmor/ges-audit-engine": "1.
|
|
7
|
-
"@greenarmor/ges-cicd-generator": "1.
|
|
8
|
-
"@greenarmor/ges-compliance-engine": "1.
|
|
9
|
-
"@greenarmor/ges-core": "1.
|
|
10
|
-
"@greenarmor/ges-doc-generator": "1.
|
|
11
|
-
"@greenarmor/ges-git-hooks": "1.
|
|
12
|
-
"@greenarmor/ges-mcp-server": "1.
|
|
13
|
-
"@greenarmor/ges-policy-engine": "1.
|
|
14
|
-
"@greenarmor/ges-report-generator": "1.
|
|
15
|
-
"@greenarmor/ges-rules-engine": "1.
|
|
16
|
-
"@greenarmor/ges-scanner-integration": "1.
|
|
17
|
-
"@greenarmor/ges-scoring-engine": "1.
|
|
18
|
-
"@greenarmor/ges-web-dashboard": "1.
|
|
6
|
+
"@greenarmor/ges-audit-engine": "1.5.0",
|
|
7
|
+
"@greenarmor/ges-cicd-generator": "1.5.0",
|
|
8
|
+
"@greenarmor/ges-compliance-engine": "1.5.0",
|
|
9
|
+
"@greenarmor/ges-core": "1.5.0",
|
|
10
|
+
"@greenarmor/ges-doc-generator": "1.5.0",
|
|
11
|
+
"@greenarmor/ges-git-hooks": "1.5.0",
|
|
12
|
+
"@greenarmor/ges-mcp-server": "1.5.0",
|
|
13
|
+
"@greenarmor/ges-policy-engine": "1.5.0",
|
|
14
|
+
"@greenarmor/ges-report-generator": "1.5.0",
|
|
15
|
+
"@greenarmor/ges-rules-engine": "1.5.0",
|
|
16
|
+
"@greenarmor/ges-scanner-integration": "1.5.0",
|
|
17
|
+
"@greenarmor/ges-scoring-engine": "1.5.0",
|
|
18
|
+
"@greenarmor/ges-web-dashboard": "1.5.0",
|
|
19
19
|
"chalk": "^5.6.2",
|
|
20
20
|
"commander": "^13.0.0"
|
|
21
21
|
},
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
"vitest": "^4.1.8"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"@inquirer/
|
|
29
|
+
"@inquirer/checkbox": "^5.2.1",
|
|
30
|
+
"@inquirer/confirm": "^6.1.1",
|
|
31
|
+
"@inquirer/input": "^5.1.2",
|
|
32
|
+
"@inquirer/select": "^5.2.1"
|
|
30
33
|
},
|
|
31
34
|
"engines": {
|
|
32
35
|
"node": ">=20.0.0"
|
|
@@ -57,7 +60,7 @@
|
|
|
57
60
|
},
|
|
58
61
|
"type": "module",
|
|
59
62
|
"types": "./dist/index.d.ts",
|
|
60
|
-
"version": "1.
|
|
63
|
+
"version": "1.5.0",
|
|
61
64
|
"scripts": {
|
|
62
65
|
"build": "tsc",
|
|
63
66
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|