@greenarmor/ges 1.0.0 → 1.1.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 +8 -0
- package/dist/commands/audit.js +43 -4
- package/dist/commands/control.d.ts +2 -0
- package/dist/commands/control.js +41 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +44 -0
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/fix.d.ts +2 -0
- package/dist/commands/fix.js +87 -0
- package/dist/commands/generate.js +1 -1
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.js +40 -0
- package/dist/commands/init.js +5 -4
- package/dist/commands/report.js +5 -5
- package/dist/utils/project.js +2 -1
- package/dist/utils/project.test.d.ts +1 -0
- package/dist/utils/project.test.js +88 -0
- package/package.json +25 -21
- package/LICENSE +0 -21
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,10 @@ import { policyCommand } from "./commands/policy.js";
|
|
|
13
13
|
import { updateCommand } from "./commands/update.js";
|
|
14
14
|
import { mcpCommand } from "./commands/mcp.js";
|
|
15
15
|
import { badgeCommand } from "./commands/badge.js";
|
|
16
|
+
import { controlCommand } from "./commands/control.js";
|
|
17
|
+
import { fixCommand } from "./commands/fix.js";
|
|
18
|
+
import { hooksCommand } from "./commands/hooks.js";
|
|
19
|
+
import { dashboardCommand } from "./commands/dashboard.js";
|
|
16
20
|
import { GESF_VERSION } from "@greenarmor/ges-core";
|
|
17
21
|
const program = new Command();
|
|
18
22
|
program
|
|
@@ -32,4 +36,8 @@ program.addCommand(policyCommand);
|
|
|
32
36
|
program.addCommand(updateCommand);
|
|
33
37
|
program.addCommand(mcpCommand);
|
|
34
38
|
program.addCommand(badgeCommand);
|
|
39
|
+
program.addCommand(controlCommand);
|
|
40
|
+
program.addCommand(fixCommand);
|
|
41
|
+
program.addCommand(hooksCommand);
|
|
42
|
+
program.addCommand(dashboardCommand);
|
|
35
43
|
program.parse();
|
package/dist/commands/audit.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { ensureGESInitialized, readJsonFile, writeJsonFile } from "../utils/project.js";
|
|
3
3
|
import { getPacksForProjectType, getAllPacks } from "@greenarmor/ges-policy-engine";
|
|
4
4
|
import { generateScoreFile, formatScoreOutput } from "@greenarmor/ges-scoring-engine";
|
|
5
|
-
import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
|
|
5
|
+
import { runAudit, runAuditIncremental, deduplicateFindings } from "@greenarmor/ges-audit-engine";
|
|
6
6
|
import { showNextStepsMenu } from "../utils/next-steps.js";
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
@@ -10,15 +10,32 @@ export const auditCommand = new Command("audit")
|
|
|
10
10
|
.description("Run a compliance audit on the project")
|
|
11
11
|
.option("--ci", "CI mode - exit with error code on failures")
|
|
12
12
|
.option("--json", "Output findings as JSON")
|
|
13
|
+
.option("--incremental", "Only rescan changed files since last audit")
|
|
13
14
|
.action(async (options) => {
|
|
14
15
|
const root = ensureGESInitialized();
|
|
15
16
|
const config = readJsonFile(path.join(root, ".ges", "config.json"));
|
|
16
17
|
console.log("\n GESF Compliance Audit");
|
|
17
18
|
console.log(" ────────────────────\n");
|
|
18
19
|
console.log(" Scanning project files...");
|
|
19
|
-
|
|
20
|
+
let rawFindings;
|
|
21
|
+
let scannedFiles;
|
|
22
|
+
if (options.incremental) {
|
|
23
|
+
const cachePath = path.join(root, ".ges", "audit-cache.json");
|
|
24
|
+
const existingCache = readJsonFile(cachePath) || {};
|
|
25
|
+
const result = runAuditIncremental(root, existingCache);
|
|
26
|
+
writeJsonFile(cachePath, result.newCache);
|
|
27
|
+
rawFindings = result.findings;
|
|
28
|
+
scannedFiles = result.scannedFiles;
|
|
29
|
+
console.log(` Scanned ${scannedFiles} files (${result.changedFiles} changed)`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const result = runAudit(root);
|
|
33
|
+
rawFindings = result.findings;
|
|
34
|
+
scannedFiles = result.scannedFiles;
|
|
35
|
+
console.log(` Scanned ${scannedFiles} files`);
|
|
36
|
+
}
|
|
20
37
|
const findings = deduplicateFindings(rawFindings);
|
|
21
|
-
console.log(
|
|
38
|
+
console.log("");
|
|
22
39
|
const frameworks = (config?.frameworks || ["GDPR", "OWASP"]);
|
|
23
40
|
const projectPacks = getPacksForProjectType(config?.project_type || "generic-web-application");
|
|
24
41
|
const packIds = new Set(projectPacks.map(p => p.id));
|
|
@@ -105,13 +122,35 @@ function applyControlOverrides(controls, overrides) {
|
|
|
105
122
|
};
|
|
106
123
|
});
|
|
107
124
|
}
|
|
125
|
+
const SCANNABLE_CATEGORIES = new Set([
|
|
126
|
+
"encryption",
|
|
127
|
+
"authentication",
|
|
128
|
+
"audit",
|
|
129
|
+
"security",
|
|
130
|
+
"database",
|
|
131
|
+
"secrets",
|
|
132
|
+
"injection",
|
|
133
|
+
"xss",
|
|
134
|
+
"infrastructure",
|
|
135
|
+
"dependencies",
|
|
136
|
+
]);
|
|
108
137
|
function updateControlsFromFindings(controls, findings) {
|
|
138
|
+
const controlsWithFindings = new Set(findings.flatMap(f => f.controlIds));
|
|
109
139
|
return controls.map(control => {
|
|
110
140
|
if (control.status === "pass" || control.status === "not-applicable")
|
|
111
141
|
return control;
|
|
112
142
|
const relevantFindings = findings.filter(f => f.controlIds.includes(control.id));
|
|
113
|
-
if (relevantFindings.length === 0)
|
|
143
|
+
if (relevantFindings.length === 0) {
|
|
144
|
+
if (SCANNABLE_CATEGORIES.has(control.category) &&
|
|
145
|
+
!controlsWithFindings.has(control.id)) {
|
|
146
|
+
return {
|
|
147
|
+
...control,
|
|
148
|
+
checks: control.checks.map(check => ({ ...check, status: "pass" })),
|
|
149
|
+
status: "pass",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
114
152
|
return control;
|
|
153
|
+
}
|
|
115
154
|
const hasCritical = relevantFindings.some(f => f.severity === "critical" || f.severity === "high");
|
|
116
155
|
const updatedChecks = control.checks.map(check => {
|
|
117
156
|
if (hasCritical) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureGESInitialized, readJsonFile, writeJsonFile } from "../utils/project.js";
|
|
3
|
+
import { getAllPacks } from "@greenarmor/ges-policy-engine";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
const VALID_STATUSES = ["pass", "fail", "warning", "not-applicable", "not-implemented"];
|
|
6
|
+
export const controlCommand = new Command("control")
|
|
7
|
+
.description("Mark a compliance control status manually")
|
|
8
|
+
.argument("<controlId>", "Control ID (e.g. GDPR-ART32-001)")
|
|
9
|
+
.argument("<status>", "Status: pass, fail, warning, not-applicable, not-implemented")
|
|
10
|
+
.option("-r, --reason <reason>", "Reason for the override")
|
|
11
|
+
.action(async (controlId, status, options) => {
|
|
12
|
+
const normalizedStatus = status;
|
|
13
|
+
if (!VALID_STATUSES.includes(normalizedStatus)) {
|
|
14
|
+
console.error(` Error: Invalid status "${status}". Valid values: ${VALID_STATUSES.join(", ")}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const allPacks = getAllPacks();
|
|
18
|
+
const allControls = allPacks.flatMap(p => p.controls);
|
|
19
|
+
const control = allControls.find(c => c.id === controlId);
|
|
20
|
+
if (!control) {
|
|
21
|
+
console.error(` Error: Unknown control ID "${controlId}".`);
|
|
22
|
+
console.error(` Available control IDs can be found in controls/ directory or via 'ges compliance'.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const root = ensureGESInitialized();
|
|
26
|
+
const overridePath = path.join(root, ".ges", "control-overrides.json");
|
|
27
|
+
const existing = readJsonFile(overridePath) || [];
|
|
28
|
+
const overrides = Array.isArray(existing) ? existing : [];
|
|
29
|
+
const filtered = overrides.filter(o => o.control_id !== controlId);
|
|
30
|
+
filtered.push({
|
|
31
|
+
control_id: controlId,
|
|
32
|
+
status: normalizedStatus,
|
|
33
|
+
reason: options.reason || `Manually set to ${normalizedStatus}`,
|
|
34
|
+
});
|
|
35
|
+
writeJsonFile(overridePath, filtered);
|
|
36
|
+
console.log(`\n [✓] Control ${controlId} marked as: ${normalizedStatus}`);
|
|
37
|
+
console.log(` ${control.name}`);
|
|
38
|
+
console.log(` Reason: ${options.reason || `Manually set to ${normalizedStatus}`}`);
|
|
39
|
+
console.log(`\n Override saved to: ${overridePath}`);
|
|
40
|
+
console.log(` Run 'ges audit' to see the updated compliance score.\n`);
|
|
41
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureGESInitialized } from "../utils/project.js";
|
|
3
|
+
import { startDashboard } from "@greenarmor/ges-web-dashboard";
|
|
4
|
+
export const dashboardCommand = new Command("dashboard")
|
|
5
|
+
.description("Start the GESF compliance web dashboard")
|
|
6
|
+
.option("-p, --port <port>", "Port number (default: 3001)")
|
|
7
|
+
.option("-h, --host <host>", "Host to bind to (default: localhost)")
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
const root = ensureGESInitialized();
|
|
10
|
+
const port = options.port ? parseInt(options.port, 10) : 3001;
|
|
11
|
+
const host = options.host || "localhost";
|
|
12
|
+
console.log("\n GESF Web Dashboard");
|
|
13
|
+
console.log(" ──────────────────\n");
|
|
14
|
+
console.log(` Starting dashboard server...`);
|
|
15
|
+
console.log(` Project: ${root}\n`);
|
|
16
|
+
try {
|
|
17
|
+
const server = startDashboard({ port, host, projectPath: root });
|
|
18
|
+
server.on("listening", () => {
|
|
19
|
+
console.log(` Dashboard running at: http://${host}:${port}`);
|
|
20
|
+
console.log(` JSON API: http://${host}:${port}/api/data`);
|
|
21
|
+
console.log(` Health check: http://${host}:${port}/health`);
|
|
22
|
+
console.log(`\n Press Ctrl+C to stop.\n`);
|
|
23
|
+
});
|
|
24
|
+
server.on("error", (err) => {
|
|
25
|
+
if (err.code === "EADDRINUSE") {
|
|
26
|
+
console.error(` Error: Port ${port} is already in use.`);
|
|
27
|
+
console.error(` Try a different port: ges dashboard --port ${port + 1}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error(` Error: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
process.on("SIGINT", () => {
|
|
35
|
+
console.log("\n Dashboard stopped.");
|
|
36
|
+
server.close();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(` Error starting dashboard: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
package/dist/commands/doctor.js
CHANGED
|
@@ -18,11 +18,11 @@ export const doctorCommand = new Command("doctor")
|
|
|
18
18
|
checks.push({ name: "GESF initialized", status: "FAIL", detail: "Run 'ges init' first" });
|
|
19
19
|
}
|
|
20
20
|
if (root) {
|
|
21
|
-
const configPath = path.join(root, GES_DIR, "config.
|
|
21
|
+
const configPath = path.join(root, GES_DIR, "config.json");
|
|
22
22
|
checks.push({
|
|
23
23
|
name: "Config file",
|
|
24
24
|
status: fs.existsSync(configPath) ? "OK" : "WARN",
|
|
25
|
-
detail: fs.existsSync(configPath) ? configPath : "config.
|
|
25
|
+
detail: fs.existsSync(configPath) ? configPath : "config.json not found",
|
|
26
26
|
});
|
|
27
27
|
const scorePath = path.join(root, GES_DIR, "score.json");
|
|
28
28
|
const score = readJsonFile(scorePath);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureGESInitialized } from "../utils/project.js";
|
|
3
|
+
import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
|
|
4
|
+
import { createAutoFixPlan, applyAutoFixAction, getNpmInstallsFromActions } from "@greenarmor/ges-mcp-server";
|
|
5
|
+
export const fixCommand = new Command("fix")
|
|
6
|
+
.description("Automatically fix security and compliance findings")
|
|
7
|
+
.option("-d, --dry-run", "Show what would be fixed without making changes")
|
|
8
|
+
.option("-r, --rules <ruleIds>", "Only fix specific rule IDs (comma-separated, e.g. CONFIG-001,SECRETS-001)")
|
|
9
|
+
.option("--ci", "CI mode - exit with error code if findings remain after fix")
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
const root = ensureGESInitialized();
|
|
12
|
+
console.log("\n GESF Auto-Fix Engine");
|
|
13
|
+
console.log(" ────────────────────\n");
|
|
14
|
+
console.log(" Scanning project files...");
|
|
15
|
+
const { findings: rawFindings, scannedFiles } = runAudit(root);
|
|
16
|
+
const findings = deduplicateFindings(rawFindings);
|
|
17
|
+
console.log(` Scanned ${scannedFiles} files`);
|
|
18
|
+
console.log(` Found ${findings.length} findings\n`);
|
|
19
|
+
if (findings.length === 0) {
|
|
20
|
+
console.log(" [✓] No issues found. Project is clean!\n");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const ruleFilter = options.rules
|
|
24
|
+
? new Set(options.rules.split(",").map((r) => r.trim()))
|
|
25
|
+
: undefined;
|
|
26
|
+
const { actions, warnings } = createAutoFixPlan(root, findings, ruleFilter);
|
|
27
|
+
if (actions.length === 0) {
|
|
28
|
+
console.log(" No auto-fixable issues found.\n");
|
|
29
|
+
console.log(" All findings require manual review:\n");
|
|
30
|
+
for (const w of warnings) {
|
|
31
|
+
console.log(` ${w}`);
|
|
32
|
+
}
|
|
33
|
+
console.log("");
|
|
34
|
+
if (options.ci)
|
|
35
|
+
process.exit(1);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const dryRun = options.dryRun === true;
|
|
39
|
+
const npmInstalls = getNpmInstallsFromActions(actions);
|
|
40
|
+
if (dryRun) {
|
|
41
|
+
console.log(` DRY RUN — ${actions.length} fixes planned (no changes applied):\n`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(` Applying ${actions.length} fixes...\n`);
|
|
45
|
+
}
|
|
46
|
+
let applied = 0;
|
|
47
|
+
let failed = 0;
|
|
48
|
+
for (const action of actions) {
|
|
49
|
+
if (dryRun) {
|
|
50
|
+
console.log(` [${action.type}] ${action.filePath}`);
|
|
51
|
+
console.log(` ${action.description} [${action.ruleId}]`);
|
|
52
|
+
applied++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const result = applyAutoFixAction(root, action);
|
|
56
|
+
if (result.applied) {
|
|
57
|
+
console.log(` [✓] [${action.type}] ${action.filePath} [${action.ruleId}]`);
|
|
58
|
+
applied++;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(` [✗] [${action.type}] ${action.filePath} [${action.ruleId}]`);
|
|
62
|
+
console.log(` ${result.error}`);
|
|
63
|
+
failed++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log(` Fixes ${dryRun ? "planned" : "applied"}: ${applied}${failed > 0 ? ` (${failed} failed)` : ""}`);
|
|
69
|
+
if (npmInstalls.length > 0) {
|
|
70
|
+
console.log("\n npm packages to install:");
|
|
71
|
+
console.log(` npm install ${npmInstalls.join(" ")}`);
|
|
72
|
+
}
|
|
73
|
+
if (warnings.length > 0) {
|
|
74
|
+
console.log("\n Manual review required:");
|
|
75
|
+
for (const w of warnings) {
|
|
76
|
+
console.log(` ${w}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log("\n Next steps:");
|
|
80
|
+
console.log(" 1. Install npm packages listed above");
|
|
81
|
+
console.log(" 2. Review changes: git diff");
|
|
82
|
+
console.log(" 3. Re-run audit: ges audit");
|
|
83
|
+
console.log("");
|
|
84
|
+
if (options.ci && (findings.length > 0 && failed < findings.length) === false && findings.length > 0) {
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -11,7 +11,7 @@ export const generateCommand = new Command("generate")
|
|
|
11
11
|
.option("--all", "Regenerate everything")
|
|
12
12
|
.action(async (options) => {
|
|
13
13
|
const root = ensureGESInitialized();
|
|
14
|
-
const config = readJsonFile(path.join(".ges", "config.json"));
|
|
14
|
+
const config = readJsonFile(path.join(root, ".ges", "config.json"));
|
|
15
15
|
if (!config) {
|
|
16
16
|
console.error(" Error: Could not read configuration.");
|
|
17
17
|
process.exit(1);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureGESInitialized } from "../utils/project.js";
|
|
3
|
+
import { installHooks, uninstallHooks } from "@greenarmor/ges-git-hooks";
|
|
4
|
+
export const hooksCommand = new Command("hooks")
|
|
5
|
+
.description("Manage GESF git hooks (pre-commit compliance enforcement)")
|
|
6
|
+
.addCommand(new Command("install")
|
|
7
|
+
.description("Install the pre-commit hook that runs ges audit before each commit")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const root = ensureGESInitialized();
|
|
10
|
+
const result = installHooks(root, ["pre-commit"]);
|
|
11
|
+
if (result.errors.length > 0) {
|
|
12
|
+
for (const err of result.errors) {
|
|
13
|
+
console.error(` Error: ${err}`);
|
|
14
|
+
}
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
for (const h of result.installed) {
|
|
18
|
+
console.log(` [✓] Installed hook: ${h}`);
|
|
19
|
+
}
|
|
20
|
+
for (const s of result.skipped) {
|
|
21
|
+
console.log(` [!] Skipped: ${s}`);
|
|
22
|
+
}
|
|
23
|
+
console.log("\n Pre-commit hook installed at: .git/hooks/pre-commit");
|
|
24
|
+
console.log(" The hook will run 'ges audit' before allowing commits.");
|
|
25
|
+
console.log(" To bypass: git commit --no-verify");
|
|
26
|
+
console.log(" To remove: ges hooks uninstall\n");
|
|
27
|
+
}))
|
|
28
|
+
.addCommand(new Command("uninstall")
|
|
29
|
+
.description("Remove the GESF pre-commit hook")
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const root = ensureGESInitialized();
|
|
32
|
+
const result = uninstallHooks(root, ["pre-commit"]);
|
|
33
|
+
for (const h of result.installed) {
|
|
34
|
+
console.log(` [✓] ${h}`);
|
|
35
|
+
}
|
|
36
|
+
for (const s of result.skipped) {
|
|
37
|
+
console.log(` [!] ${s}`);
|
|
38
|
+
}
|
|
39
|
+
console.log("\n Pre-commit hook removed.\n");
|
|
40
|
+
}));
|
package/dist/commands/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { input, select, checkbox } from "../utils/prompts.js";
|
|
3
3
|
import { PROJECT_TYPES, FRAMEWORKS, DEFAULT_FRAMEWORKS, GESF_VERSION, GES_DIR, COMPLIANCE_DIR, SECURITY_DIR, CONTROLS_DIR, POLICIES_DIR, CHECKLISTS_DIR, DOCS_DIR, REPORTS_DIR, } from "@greenarmor/ges-core";
|
|
4
4
|
import { getPacksForProjectType } from "@greenarmor/ges-policy-engine";
|
|
5
|
-
import { generateComplianceDocs, generateSecurityDocs,
|
|
5
|
+
import { generateComplianceDocs, generateSecurityDocs, generateConfigJson, generateMetadataJson, generateFrameworkVersionJson, generateScoreJson, } from "@greenarmor/ges-doc-generator";
|
|
6
6
|
import { generateAllWorkflows } from "@greenarmor/ges-cicd-generator";
|
|
7
7
|
import { writeFileSync } from "../utils/project.js";
|
|
8
8
|
import { showNextStepsMenu } from "../utils/next-steps.js";
|
|
@@ -77,8 +77,6 @@ export const initCommand = new Command("init")
|
|
|
77
77
|
for (const dir of dirs) {
|
|
78
78
|
fs.mkdirSync(path.join(process.cwd(), dir), { recursive: true });
|
|
79
79
|
}
|
|
80
|
-
const configYaml = generateConfigYaml(config);
|
|
81
|
-
writeFileSync(path.join(process.cwd(), configYaml.filePath), configYaml.content);
|
|
82
80
|
const configJson = generateConfigJson(config);
|
|
83
81
|
writeFileSync(path.join(process.cwd(), configJson.filePath), configJson.content);
|
|
84
82
|
const metadata = generateMetadataJson(config);
|
|
@@ -95,7 +93,10 @@ export const initCommand = new Command("init")
|
|
|
95
93
|
for (const doc of securityDocs) {
|
|
96
94
|
writeFileSync(path.join(process.cwd(), doc.filePath), doc.content);
|
|
97
95
|
}
|
|
98
|
-
const
|
|
96
|
+
const allProjectPacks = getPacksForProjectType(projectType);
|
|
97
|
+
const fwLower = new Set(selectedFrameworks.map((f) => f.toLowerCase()));
|
|
98
|
+
const DOMAIN_PACKS = new Set(["ai", "blockchain", "government"]);
|
|
99
|
+
const packs = allProjectPacks.filter(pack => DOMAIN_PACKS.has(pack.id.toLowerCase()) || fwLower.has(pack.id.toLowerCase()));
|
|
99
100
|
for (const pack of packs) {
|
|
100
101
|
const packDir = path.join(process.cwd(), CONTROLS_DIR, pack.id);
|
|
101
102
|
fs.mkdirSync(packDir, { recursive: true });
|
package/dist/commands/report.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { ensureGESInitialized, readJsonFile, writeFileSync } from "../utils/project.js";
|
|
3
3
|
import { getAllPacks } from "@greenarmor/ges-policy-engine";
|
|
4
|
-
import { generateMarkdownReport, generateHtmlReport } from "@greenarmor/ges-report-generator";
|
|
4
|
+
import { generateMarkdownReport, generateHtmlReport, generatePdfReport } from "@greenarmor/ges-report-generator";
|
|
5
5
|
import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
|
|
6
6
|
import { showNextStepsMenu } from "../utils/next-steps.js";
|
|
7
7
|
import * as path from "node:path";
|
|
@@ -35,6 +35,10 @@ export const reportCommand = new Command("report")
|
|
|
35
35
|
content = generateHtmlReport(reportOptions, score, controls, findings);
|
|
36
36
|
ext = "html";
|
|
37
37
|
}
|
|
38
|
+
else if (options.format === "pdf") {
|
|
39
|
+
content = generatePdfReport(reportOptions, score, controls, findings);
|
|
40
|
+
ext = "pdf";
|
|
41
|
+
}
|
|
38
42
|
else {
|
|
39
43
|
content = generateMarkdownReport(reportOptions, score, controls, findings);
|
|
40
44
|
ext = "md";
|
|
@@ -44,9 +48,5 @@ export const reportCommand = new Command("report")
|
|
|
44
48
|
writeFileSync(outputPath, content);
|
|
45
49
|
console.log(` Report generated: ${outputPath}`);
|
|
46
50
|
console.log(` ${findings.length} security findings included\n`);
|
|
47
|
-
if (options.format === "pdf") {
|
|
48
|
-
console.log(" Note: PDF generation requires pandoc:");
|
|
49
|
-
console.log(` pandoc ${outputPath} -o ${outputPath.replace(".md", ".pdf")}\n`);
|
|
50
|
-
}
|
|
51
51
|
await showNextStepsMenu("report");
|
|
52
52
|
});
|
package/dist/utils/project.js
CHANGED
|
@@ -4,7 +4,8 @@ export const GES_DIR = ".ges";
|
|
|
4
4
|
export function findProjectRoot(startDir = process.cwd()) {
|
|
5
5
|
let dir = startDir;
|
|
6
6
|
while (dir !== path.dirname(dir)) {
|
|
7
|
-
if (fs.existsSync(path.join(dir, GES_DIR, "config.
|
|
7
|
+
if (fs.existsSync(path.join(dir, GES_DIR, "config.json")) ||
|
|
8
|
+
fs.existsSync(path.join(dir, GES_DIR, "config.yaml"))) {
|
|
8
9
|
return dir;
|
|
9
10
|
}
|
|
10
11
|
dir = path.dirname(dir);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { findProjectRoot, readJsonFile, writeJsonFile, writeFileSync, GES_DIR, } from "./project.js";
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let origCwd;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gesf-cli-test-"));
|
|
10
|
+
origCwd = process.cwd();
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
process.chdir(origCwd);
|
|
14
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
describe("findProjectRoot", () => {
|
|
17
|
+
it("finds root with config.json", () => {
|
|
18
|
+
fs.mkdirSync(path.join(tmpDir, GES_DIR));
|
|
19
|
+
fs.writeFileSync(path.join(tmpDir, GES_DIR, "config.json"), "{}");
|
|
20
|
+
expect(findProjectRoot(tmpDir)).toBe(tmpDir);
|
|
21
|
+
});
|
|
22
|
+
it("finds root with config.yaml (backwards compat)", () => {
|
|
23
|
+
fs.mkdirSync(path.join(tmpDir, GES_DIR));
|
|
24
|
+
fs.writeFileSync(path.join(tmpDir, GES_DIR, "config.yaml"), "name: test");
|
|
25
|
+
expect(findProjectRoot(tmpDir)).toBe(tmpDir);
|
|
26
|
+
});
|
|
27
|
+
it("returns null when no config found", () => {
|
|
28
|
+
expect(findProjectRoot(tmpDir)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
it("finds root from nested subdirectory", () => {
|
|
31
|
+
fs.mkdirSync(path.join(tmpDir, GES_DIR));
|
|
32
|
+
fs.writeFileSync(path.join(tmpDir, GES_DIR, "config.json"), "{}");
|
|
33
|
+
const nested = path.join(tmpDir, "src", "deep", "path");
|
|
34
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
35
|
+
expect(findProjectRoot(nested)).toBe(tmpDir);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("readJsonFile", () => {
|
|
39
|
+
it("reads valid JSON", () => {
|
|
40
|
+
const filePath = path.join(tmpDir, "test.json");
|
|
41
|
+
fs.writeFileSync(filePath, '{"name":"test","value":42}');
|
|
42
|
+
const result = readJsonFile(filePath);
|
|
43
|
+
expect(result).not.toBeNull();
|
|
44
|
+
expect(result.name).toBe("test");
|
|
45
|
+
expect(result.value).toBe(42);
|
|
46
|
+
});
|
|
47
|
+
it("returns null for missing file", () => {
|
|
48
|
+
expect(readJsonFile(path.join(tmpDir, "nonexistent.json"))).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it("returns null for invalid JSON", () => {
|
|
51
|
+
const filePath = path.join(tmpDir, "bad.json");
|
|
52
|
+
fs.writeFileSync(filePath, "{not valid json}");
|
|
53
|
+
expect(readJsonFile(filePath)).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("writeJsonFile", () => {
|
|
57
|
+
it("writes JSON to file", () => {
|
|
58
|
+
const filePath = path.join(tmpDir, "output.json");
|
|
59
|
+
writeJsonFile(filePath, { name: "test", items: [1, 2, 3] });
|
|
60
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
61
|
+
const parsed = JSON.parse(content);
|
|
62
|
+
expect(parsed.name).toBe("test");
|
|
63
|
+
expect(parsed.items).toEqual([1, 2, 3]);
|
|
64
|
+
});
|
|
65
|
+
it("creates parent directories", () => {
|
|
66
|
+
const filePath = path.join(tmpDir, "nested", "dir", "output.json");
|
|
67
|
+
writeJsonFile(filePath, { ok: true });
|
|
68
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("writeFileSync", () => {
|
|
72
|
+
it("writes content to file", () => {
|
|
73
|
+
const filePath = path.join(tmpDir, "test.txt");
|
|
74
|
+
writeFileSync(filePath, "hello world");
|
|
75
|
+
expect(fs.readFileSync(filePath, "utf-8")).toBe("hello world");
|
|
76
|
+
});
|
|
77
|
+
it("creates parent directories if needed", () => {
|
|
78
|
+
const filePath = path.join(tmpDir, "a", "b", "c", "file.txt");
|
|
79
|
+
writeFileSync(filePath, "nested");
|
|
80
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it("overwrites existing file", () => {
|
|
83
|
+
const filePath = path.join(tmpDir, "overwrite.txt");
|
|
84
|
+
writeFileSync(filePath, "first");
|
|
85
|
+
writeFileSync(filePath, "second");
|
|
86
|
+
expect(fs.readFileSync(filePath, "utf-8")).toBe("second");
|
|
87
|
+
});
|
|
88
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greenarmor/ges",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Green Engineering Standard Framework - Compliance-as-Code CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,23 +11,32 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
17
|
+
"prepublishOnly": "pnpm -r run build",
|
|
18
|
+
"test": "vitest run"
|
|
19
|
+
},
|
|
14
20
|
"dependencies": {
|
|
15
|
-
"
|
|
16
|
-
"@greenarmor/ges-
|
|
17
|
-
"@greenarmor/ges-compliance-engine": "
|
|
18
|
-
"@greenarmor/ges-
|
|
19
|
-
"@greenarmor/ges-
|
|
20
|
-
"@greenarmor/ges-
|
|
21
|
-
"@greenarmor/ges-
|
|
22
|
-
"@greenarmor/ges-rules-engine": "
|
|
23
|
-
"@greenarmor/ges-scanner-integration": "
|
|
24
|
-
"@greenarmor/ges-
|
|
25
|
-
"@greenarmor/ges-
|
|
26
|
-
"@greenarmor/ges-
|
|
21
|
+
"@greenarmor/ges-audit-engine": "workspace:*",
|
|
22
|
+
"@greenarmor/ges-cicd-generator": "workspace:*",
|
|
23
|
+
"@greenarmor/ges-compliance-engine": "workspace:*",
|
|
24
|
+
"@greenarmor/ges-core": "workspace:*",
|
|
25
|
+
"@greenarmor/ges-doc-generator": "workspace:*",
|
|
26
|
+
"@greenarmor/ges-policy-engine": "workspace:*",
|
|
27
|
+
"@greenarmor/ges-report-generator": "workspace:*",
|
|
28
|
+
"@greenarmor/ges-rules-engine": "workspace:*",
|
|
29
|
+
"@greenarmor/ges-scanner-integration": "workspace:*",
|
|
30
|
+
"@greenarmor/ges-scoring-engine": "workspace:*",
|
|
31
|
+
"@greenarmor/ges-mcp-server": "workspace:*",
|
|
32
|
+
"@greenarmor/ges-git-hooks": "workspace:*",
|
|
33
|
+
"@greenarmor/ges-web-dashboard": "workspace:*",
|
|
34
|
+
"commander": "^13.0.0"
|
|
27
35
|
},
|
|
28
36
|
"devDependencies": {
|
|
29
37
|
"@types/node": "^22.0.0",
|
|
30
|
-
"typescript": "^6.0.0"
|
|
38
|
+
"typescript": "^6.0.0",
|
|
39
|
+
"vitest": "^4.1.8"
|
|
31
40
|
},
|
|
32
41
|
"engines": {
|
|
33
42
|
"node": ">=20.0.0"
|
|
@@ -48,10 +57,5 @@
|
|
|
48
57
|
"type": "git",
|
|
49
58
|
"url": "https://github.com/greenarmor/gesf"
|
|
50
59
|
},
|
|
51
|
-
"homepage": "https://github.com/greenarmor/gesf"
|
|
52
|
-
|
|
53
|
-
"build": "tsc",
|
|
54
|
-
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
55
|
-
"test": "echo \"no tests yet\""
|
|
56
|
-
}
|
|
57
|
-
}
|
|
60
|
+
"homepage": "https://github.com/greenarmor/gesf"
|
|
61
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025–2026 greenarmor
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|