@nodatachat/guard 2.0.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/LICENSE.md +28 -0
- package/README.md +120 -0
- package/dist/activation.d.ts +8 -0
- package/dist/activation.js +110 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +458 -0
- package/dist/code-scanner.d.ts +14 -0
- package/dist/code-scanner.js +309 -0
- package/dist/db-scanner.d.ts +7 -0
- package/dist/db-scanner.js +185 -0
- package/dist/fixers/fix-csrf.d.ts +9 -0
- package/dist/fixers/fix-csrf.js +113 -0
- package/dist/fixers/fix-gitignore.d.ts +9 -0
- package/dist/fixers/fix-gitignore.js +71 -0
- package/dist/fixers/fix-headers.d.ts +9 -0
- package/dist/fixers/fix-headers.js +118 -0
- package/dist/fixers/fix-pii-encrypt.d.ts +9 -0
- package/dist/fixers/fix-pii-encrypt.js +298 -0
- package/dist/fixers/fix-rate-limit.d.ts +9 -0
- package/dist/fixers/fix-rate-limit.js +102 -0
- package/dist/fixers/fix-rls.d.ts +9 -0
- package/dist/fixers/fix-rls.js +243 -0
- package/dist/fixers/fix-routes-auth.d.ts +9 -0
- package/dist/fixers/fix-routes-auth.js +82 -0
- package/dist/fixers/fix-secrets.d.ts +9 -0
- package/dist/fixers/fix-secrets.js +132 -0
- package/dist/fixers/index.d.ts +11 -0
- package/dist/fixers/index.js +37 -0
- package/dist/fixers/registry.d.ts +25 -0
- package/dist/fixers/registry.js +249 -0
- package/dist/fixers/scheduler.d.ts +9 -0
- package/dist/fixers/scheduler.js +254 -0
- package/dist/fixers/types.d.ts +160 -0
- package/dist/fixers/types.js +11 -0
- package/dist/reporter.d.ts +28 -0
- package/dist/reporter.js +185 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.js +5 -0
- package/package.json +61 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════
|
|
3
|
+
// Guard Capsule — Fixer Registry
|
|
4
|
+
//
|
|
5
|
+
// Maps finding types to fixer modules.
|
|
6
|
+
// Orchestrates: analyze → plan → preview → apply → verify
|
|
7
|
+
// ═══════════════════════════════════════════════════════════
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getFixer = getFixer;
|
|
10
|
+
exports.getAllFixers = getAllFixers;
|
|
11
|
+
exports.getFixersByPriority = getFixersByPriority;
|
|
12
|
+
exports.runCapsule = runCapsule;
|
|
13
|
+
exports.sendNotifications = sendNotifications;
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
// Import fixers (will be implemented one by one)
|
|
16
|
+
const fix_pii_encrypt_1 = require("./fix-pii-encrypt");
|
|
17
|
+
const fix_rls_1 = require("./fix-rls");
|
|
18
|
+
const fix_secrets_1 = require("./fix-secrets");
|
|
19
|
+
const fix_routes_auth_1 = require("./fix-routes-auth");
|
|
20
|
+
const fix_headers_1 = require("./fix-headers");
|
|
21
|
+
const fix_csrf_1 = require("./fix-csrf");
|
|
22
|
+
const fix_rate_limit_1 = require("./fix-rate-limit");
|
|
23
|
+
const fix_gitignore_1 = require("./fix-gitignore");
|
|
24
|
+
// ── Registry ──
|
|
25
|
+
const ALL_FIXERS = [
|
|
26
|
+
new fix_pii_encrypt_1.PiiEncryptFixer(),
|
|
27
|
+
new fix_rls_1.RlsFixer(),
|
|
28
|
+
new fix_secrets_1.SecretsFixer(),
|
|
29
|
+
new fix_routes_auth_1.RoutesAuthFixer(),
|
|
30
|
+
new fix_headers_1.HeadersFixer(),
|
|
31
|
+
new fix_csrf_1.CsrfFixer(),
|
|
32
|
+
new fix_rate_limit_1.RateLimitFixer(),
|
|
33
|
+
new fix_gitignore_1.GitignoreFixer(),
|
|
34
|
+
];
|
|
35
|
+
function getFixer(category) {
|
|
36
|
+
return ALL_FIXERS.find(f => f.category === category);
|
|
37
|
+
}
|
|
38
|
+
function getAllFixers() {
|
|
39
|
+
return [...ALL_FIXERS];
|
|
40
|
+
}
|
|
41
|
+
function getFixersByPriority() {
|
|
42
|
+
// Critical fixers first, then high, then medium
|
|
43
|
+
const priority = [
|
|
44
|
+
"secrets", // Critical — hardcoded secrets are immediate risk
|
|
45
|
+
"pii-encrypt", // Critical — exposed PII
|
|
46
|
+
"rls", // High — DB access control
|
|
47
|
+
"routes-auth", // High — API exposure
|
|
48
|
+
"headers", // High — browser security
|
|
49
|
+
"csrf", // High — request forgery
|
|
50
|
+
"rate-limit", // Medium — abuse prevention
|
|
51
|
+
"gitignore", // Medium — prevention
|
|
52
|
+
];
|
|
53
|
+
return priority.map(c => getFixer(c)).filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
async function runCapsule(context, options, onProgress) {
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const fixers = options.fixers
|
|
58
|
+
? options.fixers.map(c => getFixer(c)).filter(Boolean)
|
|
59
|
+
: getFixersByPriority();
|
|
60
|
+
// Filter out skipped fixers from config
|
|
61
|
+
const skipList = context.config.fix?.skipFixers || [];
|
|
62
|
+
const activeFixer = fixers.filter(f => !skipList.includes(f.category));
|
|
63
|
+
const plans = [];
|
|
64
|
+
const results = [];
|
|
65
|
+
// ── Phase 1: Analyze (generate plans) ──
|
|
66
|
+
onProgress?.("Analyzing findings...");
|
|
67
|
+
for (const fixer of activeFixer) {
|
|
68
|
+
onProgress?.(` Analyzing ${fixer.name}...`);
|
|
69
|
+
try {
|
|
70
|
+
const plan = await fixer.analyze(context);
|
|
71
|
+
if (plan.actions.length > 0) {
|
|
72
|
+
plans.push(plan);
|
|
73
|
+
onProgress?.(` ${fixer.name}: ${plan.totalActions} actions (${plan.autoFixable} auto, ${plan.manualRequired} manual)`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
onProgress?.(` ${fixer.name}: no issues found`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
onProgress?.(` ${fixer.name}: analysis failed — ${err instanceof Error ? err.message : err}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (options.mode === "plan" || options.dryRun) {
|
|
84
|
+
return {
|
|
85
|
+
plans,
|
|
86
|
+
results: [],
|
|
87
|
+
totalActions: plans.reduce((s, p) => s + p.totalActions, 0),
|
|
88
|
+
applied: 0,
|
|
89
|
+
failed: 0,
|
|
90
|
+
skipped: 0,
|
|
91
|
+
manualPending: plans.reduce((s, p) => s + p.manualRequired, 0),
|
|
92
|
+
estimatedScoreImpact: plans.reduce((s, p) => s + p.estimatedScoreImpact, 0),
|
|
93
|
+
proofHash: hashPlans(plans),
|
|
94
|
+
duration: Date.now() - startTime,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// ── Phase 2: Apply ──
|
|
98
|
+
onProgress?.("Applying fixes...");
|
|
99
|
+
for (const plan of plans) {
|
|
100
|
+
const fixer = getFixer(plan.fixer);
|
|
101
|
+
if (!fixer)
|
|
102
|
+
continue;
|
|
103
|
+
// Check if this fixer needs approval
|
|
104
|
+
const needsApproval = context.config.fix?.requireApproval?.includes(plan.fixer);
|
|
105
|
+
if (needsApproval && !options.interactive) {
|
|
106
|
+
onProgress?.(` ${fixer.name}: skipped (requires approval)`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
onProgress?.(` Applying ${fixer.name} (${plan.autoFixable} actions)...`);
|
|
110
|
+
try {
|
|
111
|
+
const result = await fixer.apply(plan, options.actionIds);
|
|
112
|
+
results.push(result);
|
|
113
|
+
onProgress?.(` ${fixer.name}: ${result.applied} applied, ${result.failed} failed, ${result.skipped} skipped`);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
onProgress?.(` ${fixer.name}: apply failed — ${err instanceof Error ? err.message : err}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ── Phase 3: Verify (optional) ──
|
|
120
|
+
if (options.mode === "verify") {
|
|
121
|
+
onProgress?.("Verifying fixes...");
|
|
122
|
+
for (const result of results) {
|
|
123
|
+
if (result.applied === 0)
|
|
124
|
+
continue;
|
|
125
|
+
const fixer = getFixer(result.fixer);
|
|
126
|
+
if (!fixer)
|
|
127
|
+
continue;
|
|
128
|
+
const verified = await fixer.verify(result);
|
|
129
|
+
onProgress?.(` ${fixer.name}: ${verified ? "VERIFIED" : "VERIFICATION FAILED"}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const totalApplied = results.reduce((s, r) => s + r.applied, 0);
|
|
133
|
+
const totalFailed = results.reduce((s, r) => s + r.failed, 0);
|
|
134
|
+
const totalSkipped = results.reduce((s, r) => s + r.skipped, 0);
|
|
135
|
+
const totalManual = results.reduce((s, r) => s + r.manualPending, 0);
|
|
136
|
+
return {
|
|
137
|
+
plans,
|
|
138
|
+
results,
|
|
139
|
+
totalActions: plans.reduce((s, p) => s + p.totalActions, 0),
|
|
140
|
+
applied: totalApplied,
|
|
141
|
+
failed: totalFailed,
|
|
142
|
+
skipped: totalSkipped,
|
|
143
|
+
manualPending: totalManual,
|
|
144
|
+
estimatedScoreImpact: plans.reduce((s, p) => s + p.estimatedScoreImpact, 0),
|
|
145
|
+
proofHash: hashResults(results),
|
|
146
|
+
duration: Date.now() - startTime,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ── Notification Dispatcher ──
|
|
150
|
+
async function sendNotifications(config, event, payload, onLog) {
|
|
151
|
+
if (!config.notify)
|
|
152
|
+
return;
|
|
153
|
+
if (!config.notify.onEvents.includes(event))
|
|
154
|
+
return;
|
|
155
|
+
// Email (via NoData API — we relay, never see content)
|
|
156
|
+
if (config.notify.email?.length) {
|
|
157
|
+
for (const email of config.notify.email) {
|
|
158
|
+
try {
|
|
159
|
+
await fetch("https://nodatachat.com/api/guard/notify", {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
body: JSON.stringify({ channel: "email", to: email, payload }),
|
|
163
|
+
signal: AbortSignal.timeout(5000),
|
|
164
|
+
});
|
|
165
|
+
onLog?.(` Notified: ${email}`);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
onLog?.(` Email notification failed: ${email}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Webhook (direct — customer's own endpoint)
|
|
173
|
+
if (config.notify.webhook?.length) {
|
|
174
|
+
for (const url of config.notify.webhook) {
|
|
175
|
+
try {
|
|
176
|
+
await fetch(url, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json", "X-NoData-Event": event },
|
|
179
|
+
body: JSON.stringify(payload),
|
|
180
|
+
signal: AbortSignal.timeout(5000),
|
|
181
|
+
});
|
|
182
|
+
onLog?.(` Webhook sent: ${url}`);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
onLog?.(` Webhook failed: ${url}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Slack
|
|
190
|
+
if (config.notify.slack) {
|
|
191
|
+
const { webhookUrl, channel, mentionOn } = config.notify.slack;
|
|
192
|
+
const shouldMention = mentionOn === "critical" && payload.critical > 0
|
|
193
|
+
|| mentionOn === "drop" && payload.delta < 0;
|
|
194
|
+
const emoji = payload.delta > 0 ? ":arrow_up:" : payload.delta < 0 ? ":arrow_down:" : ":white_check_mark:";
|
|
195
|
+
const mention = shouldMention ? "<!channel> " : "";
|
|
196
|
+
const text = [
|
|
197
|
+
`${mention}${emoji} *NoData Guard* — ${event.replace(/-/g, " ")}`,
|
|
198
|
+
`Score: *${payload.score}%*${payload.previousScore != null ? ` (was ${payload.previousScore}%)` : ""}`,
|
|
199
|
+
payload.critical > 0 ? `:red_circle: ${payload.critical} critical` : null,
|
|
200
|
+
payload.high > 0 ? `:orange_circle: ${payload.high} high` : null,
|
|
201
|
+
payload.fixesApplied ? `:wrench: ${payload.fixesApplied} fixes applied` : null,
|
|
202
|
+
payload.dashboardUrl ? `<${payload.dashboardUrl}|View Dashboard>` : null,
|
|
203
|
+
].filter(Boolean).join("\n");
|
|
204
|
+
try {
|
|
205
|
+
await fetch(webhookUrl, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "Content-Type": "application/json" },
|
|
208
|
+
body: JSON.stringify({ text, channel }),
|
|
209
|
+
signal: AbortSignal.timeout(5000),
|
|
210
|
+
});
|
|
211
|
+
onLog?.(" Slack notified");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
onLog?.(" Slack notification failed");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Telegram
|
|
218
|
+
if (config.notify.telegram) {
|
|
219
|
+
const { botToken, chatId } = config.notify.telegram;
|
|
220
|
+
const emoji = payload.delta > 0 ? "+" : payload.delta < 0 ? "" : "";
|
|
221
|
+
const text = [
|
|
222
|
+
`NoData Guard — ${event.replace(/-/g, " ")}`,
|
|
223
|
+
`Score: ${payload.score}%${payload.previousScore != null ? ` (${emoji}${payload.delta})` : ""}`,
|
|
224
|
+
payload.critical > 0 ? `Critical: ${payload.critical}` : null,
|
|
225
|
+
payload.fixesApplied ? `Fixes: ${payload.fixesApplied} applied` : null,
|
|
226
|
+
].filter(Boolean).join("\n");
|
|
227
|
+
try {
|
|
228
|
+
await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" }),
|
|
232
|
+
signal: AbortSignal.timeout(5000),
|
|
233
|
+
});
|
|
234
|
+
onLog?.(" Telegram notified");
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
onLog?.(" Telegram notification failed");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ── Helpers ──
|
|
242
|
+
function hashPlans(plans) {
|
|
243
|
+
const data = plans.map(p => `${p.fixer}:${p.totalActions}:${p.actions.map(a => a.id).join(",")}`).join("|");
|
|
244
|
+
return (0, crypto_1.createHash)("sha256").update(data).digest("hex");
|
|
245
|
+
}
|
|
246
|
+
function hashResults(results) {
|
|
247
|
+
const data = results.map(r => `${r.fixer}:${r.applied}:${r.proofHash}`).join("|");
|
|
248
|
+
return (0, crypto_1.createHash)("sha256").update(data).digest("hex");
|
|
249
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CapsuleConfig, CITemplate } from "./types";
|
|
2
|
+
export declare function generateGitHubActions(config: CapsuleConfig): CITemplate;
|
|
3
|
+
export declare function generateGitLabCI(config: CapsuleConfig): CITemplate;
|
|
4
|
+
export declare function generateBitbucket(config: CapsuleConfig): CITemplate;
|
|
5
|
+
export declare function installSchedule(projectDir: string, config: CapsuleConfig, provider?: "github-actions" | "gitlab-ci" | "bitbucket" | "auto"): {
|
|
6
|
+
files: string[];
|
|
7
|
+
messages: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function generateDefaultConfig(overrides?: Partial<CapsuleConfig>): CapsuleConfig;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════
|
|
3
|
+
// Guard Capsule — Scheduler
|
|
4
|
+
//
|
|
5
|
+
// Generates CI/CD workflow files and cron configurations.
|
|
6
|
+
// Supports: GitHub Actions, GitLab CI, Bitbucket Pipelines,
|
|
7
|
+
// local cron (node-cron), and webhook triggers.
|
|
8
|
+
// ═══════════════════════════════════════════════════════════
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.generateGitHubActions = generateGitHubActions;
|
|
11
|
+
exports.generateGitLabCI = generateGitLabCI;
|
|
12
|
+
exports.generateBitbucket = generateBitbucket;
|
|
13
|
+
exports.installSchedule = installSchedule;
|
|
14
|
+
exports.generateDefaultConfig = generateDefaultConfig;
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = require("path");
|
|
17
|
+
const PRESETS = {
|
|
18
|
+
daily: "0 3 * * *", // Every day at 3am
|
|
19
|
+
weekly: "0 3 * * 1", // Every Monday at 3am
|
|
20
|
+
monthly: "0 3 1 * *", // First of month at 3am
|
|
21
|
+
};
|
|
22
|
+
// ── GitHub Actions Workflow ──
|
|
23
|
+
function generateGitHubActions(config) {
|
|
24
|
+
const cron = config.schedule?.cron || PRESETS[config.schedule?.preset || "weekly"];
|
|
25
|
+
const failOn = config.scan?.failOn || "critical";
|
|
26
|
+
const hasDb = !!config.scan?.dbUrl;
|
|
27
|
+
const content = `# NoData Guard — Automated Security Scan
|
|
28
|
+
# Generated by @nodatachat/guard
|
|
29
|
+
# Runs: ${config.schedule?.preset || "weekly"} (${cron})
|
|
30
|
+
|
|
31
|
+
name: NoData Guard Security Scan
|
|
32
|
+
|
|
33
|
+
on:
|
|
34
|
+
schedule:
|
|
35
|
+
- cron: '${cron}'
|
|
36
|
+
push:
|
|
37
|
+
branches: [main, master]
|
|
38
|
+
pull_request:
|
|
39
|
+
branches: [main, master]
|
|
40
|
+
workflow_dispatch: # Manual trigger
|
|
41
|
+
|
|
42
|
+
jobs:
|
|
43
|
+
security-scan:
|
|
44
|
+
name: Guard Security Scan
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
timeout-minutes: 10
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- uses: actions/setup-node@v4
|
|
52
|
+
with:
|
|
53
|
+
node-version: '20'
|
|
54
|
+
|
|
55
|
+
- name: Install Guard
|
|
56
|
+
run: npm install -g @nodatachat/guard
|
|
57
|
+
|
|
58
|
+
- name: Run Security Scan
|
|
59
|
+
run: |
|
|
60
|
+
nodata-guard \\
|
|
61
|
+
--license-key \${{ secrets.NDC_LICENSE }} \\${hasDb ? `\n --db \${{ secrets.DATABASE_URL }} \\` : ""}
|
|
62
|
+
--ci \\
|
|
63
|
+
--fail-on ${failOn} \\
|
|
64
|
+
--output ./guard-reports
|
|
65
|
+
env:
|
|
66
|
+
NDC_LICENSE: \${{ secrets.NDC_LICENSE }}${hasDb ? "\n DATABASE_URL: ${{ secrets.DATABASE_URL }}" : ""}
|
|
67
|
+
|
|
68
|
+
- name: Upload Report
|
|
69
|
+
if: always()
|
|
70
|
+
uses: actions/upload-artifact@v4
|
|
71
|
+
with:
|
|
72
|
+
name: guard-report-\${{ github.sha }}
|
|
73
|
+
path: ./guard-reports/
|
|
74
|
+
retention-days: 90
|
|
75
|
+
|
|
76
|
+
- name: Comment PR with Score
|
|
77
|
+
if: github.event_name == 'pull_request'
|
|
78
|
+
uses: actions/github-script@v7
|
|
79
|
+
with:
|
|
80
|
+
script: |
|
|
81
|
+
const fs = require('fs');
|
|
82
|
+
const reports = fs.readdirSync('./guard-reports').filter(f => f.endsWith('.json') && f.includes('metadata'));
|
|
83
|
+
if (reports.length === 0) return;
|
|
84
|
+
const meta = JSON.parse(fs.readFileSync('./guard-reports/' + reports[0], 'utf-8'));
|
|
85
|
+
const score = meta.scores?.overall || '?';
|
|
86
|
+
const critical = meta.issues?.critical || 0;
|
|
87
|
+
const high = meta.issues?.high || 0;
|
|
88
|
+
const emoji = score >= 80 ? ':white_check_mark:' : score >= 60 ? ':warning:' : ':x:';
|
|
89
|
+
github.rest.issues.createComment({
|
|
90
|
+
issue_number: context.issue.number,
|
|
91
|
+
owner: context.repo.owner,
|
|
92
|
+
repo: context.repo.repo,
|
|
93
|
+
body: \`## \${emoji} NoData Guard — Score: \${score}%\\n\\n| Critical | High | Coverage |\\n|---|---|---|\\n| \${critical} | \${high} | \${meta.code_summary?.encryption_coverage_percent || '?'}% |\\n\\n> Your code never left the runner. [View full report in artifacts.]\`
|
|
94
|
+
});
|
|
95
|
+
`;
|
|
96
|
+
return {
|
|
97
|
+
provider: "github-actions",
|
|
98
|
+
filename: ".github/workflows/nodata-guard.yml",
|
|
99
|
+
content,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ── GitLab CI ──
|
|
103
|
+
function generateGitLabCI(config) {
|
|
104
|
+
const cron = config.schedule?.cron || PRESETS[config.schedule?.preset || "weekly"];
|
|
105
|
+
const failOn = config.scan?.failOn || "critical";
|
|
106
|
+
const hasDb = !!config.scan?.dbUrl;
|
|
107
|
+
const content = `# NoData Guard — Automated Security Scan
|
|
108
|
+
# Generated by @nodatachat/guard
|
|
109
|
+
|
|
110
|
+
stages:
|
|
111
|
+
- security
|
|
112
|
+
|
|
113
|
+
nodata-guard:
|
|
114
|
+
stage: security
|
|
115
|
+
image: node:20
|
|
116
|
+
rules:
|
|
117
|
+
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
118
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
119
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
120
|
+
script:
|
|
121
|
+
- npm install -g @nodatachat/guard
|
|
122
|
+
- nodata-guard --license-key $NDC_LICENSE${hasDb ? " --db $DATABASE_URL" : ""} --ci --fail-on ${failOn} --output ./guard-reports
|
|
123
|
+
artifacts:
|
|
124
|
+
paths:
|
|
125
|
+
- guard-reports/
|
|
126
|
+
expire_in: 90 days
|
|
127
|
+
when: always
|
|
128
|
+
variables:
|
|
129
|
+
NDC_LICENSE: $NDC_LICENSE${hasDb ? "\n DATABASE_URL: $DATABASE_URL" : ""}
|
|
130
|
+
|
|
131
|
+
# Add to CI/CD > Schedules: cron "${cron}"
|
|
132
|
+
`;
|
|
133
|
+
return {
|
|
134
|
+
provider: "gitlab-ci",
|
|
135
|
+
filename: ".gitlab-ci-guard.yml",
|
|
136
|
+
content,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ── Bitbucket Pipelines ──
|
|
140
|
+
function generateBitbucket(config) {
|
|
141
|
+
const failOn = config.scan?.failOn || "critical";
|
|
142
|
+
const hasDb = !!config.scan?.dbUrl;
|
|
143
|
+
const content = `# NoData Guard — Automated Security Scan
|
|
144
|
+
# Generated by @nodatachat/guard
|
|
145
|
+
|
|
146
|
+
pipelines:
|
|
147
|
+
default:
|
|
148
|
+
- step:
|
|
149
|
+
name: NoData Guard Security Scan
|
|
150
|
+
image: node:20
|
|
151
|
+
script:
|
|
152
|
+
- npm install -g @nodatachat/guard
|
|
153
|
+
- nodata-guard --license-key $NDC_LICENSE${hasDb ? " --db $DATABASE_URL" : ""} --ci --fail-on ${failOn}
|
|
154
|
+
artifacts:
|
|
155
|
+
- nodata-full-report.json
|
|
156
|
+
- nodata-metadata-only.json
|
|
157
|
+
|
|
158
|
+
custom:
|
|
159
|
+
nodata-guard:
|
|
160
|
+
- step:
|
|
161
|
+
name: Manual Security Scan
|
|
162
|
+
image: node:20
|
|
163
|
+
script:
|
|
164
|
+
- npm install -g @nodatachat/guard
|
|
165
|
+
- nodata-guard --license-key $NDC_LICENSE${hasDb ? " --db $DATABASE_URL" : ""} --ci --fail-on ${failOn}
|
|
166
|
+
`;
|
|
167
|
+
return {
|
|
168
|
+
provider: "bitbucket",
|
|
169
|
+
filename: "bitbucket-pipelines-guard.yml",
|
|
170
|
+
content,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// ── Write CI config to project ──
|
|
174
|
+
function installSchedule(projectDir, config, provider) {
|
|
175
|
+
const files = [];
|
|
176
|
+
const messages = [];
|
|
177
|
+
// Auto-detect provider
|
|
178
|
+
let detected = provider;
|
|
179
|
+
if (!detected || detected === "auto") {
|
|
180
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectDir, ".github")))
|
|
181
|
+
detected = "github-actions";
|
|
182
|
+
else if ((0, fs_1.existsSync)((0, path_1.join)(projectDir, ".gitlab-ci.yml")))
|
|
183
|
+
detected = "gitlab-ci";
|
|
184
|
+
else if ((0, fs_1.existsSync)((0, path_1.join)(projectDir, "bitbucket-pipelines.yml")))
|
|
185
|
+
detected = "bitbucket";
|
|
186
|
+
else
|
|
187
|
+
detected = "github-actions"; // Default
|
|
188
|
+
}
|
|
189
|
+
let template;
|
|
190
|
+
switch (detected) {
|
|
191
|
+
case "github-actions":
|
|
192
|
+
template = generateGitHubActions(config);
|
|
193
|
+
break;
|
|
194
|
+
case "gitlab-ci":
|
|
195
|
+
template = generateGitLabCI(config);
|
|
196
|
+
break;
|
|
197
|
+
case "bitbucket":
|
|
198
|
+
template = generateBitbucket(config);
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
template = generateGitHubActions(config);
|
|
202
|
+
}
|
|
203
|
+
// Write the file
|
|
204
|
+
const filePath = (0, path_1.join)(projectDir, template.filename);
|
|
205
|
+
const dir = (0, path_1.join)(projectDir, ...template.filename.split("/").slice(0, -1));
|
|
206
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
207
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
208
|
+
(0, fs_1.writeFileSync)(filePath, template.content, "utf-8");
|
|
209
|
+
files.push(filePath);
|
|
210
|
+
messages.push(`Created ${template.filename}`);
|
|
211
|
+
// Write .nodata-guard.json config
|
|
212
|
+
const configPath = (0, path_1.join)(projectDir, ".nodata-guard.json");
|
|
213
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
214
|
+
files.push(configPath);
|
|
215
|
+
messages.push("Created .nodata-guard.json");
|
|
216
|
+
// Remind about secrets
|
|
217
|
+
messages.push("");
|
|
218
|
+
messages.push("Required secrets:");
|
|
219
|
+
messages.push(" NDC_LICENSE — Your NoData Guard license key");
|
|
220
|
+
if (config.scan?.dbUrl) {
|
|
221
|
+
messages.push(" DATABASE_URL — Database connection string");
|
|
222
|
+
}
|
|
223
|
+
messages.push("");
|
|
224
|
+
messages.push(`Schedule: ${config.schedule?.preset || "weekly"} (${config.schedule?.cron || PRESETS[config.schedule?.preset || "weekly"]})`);
|
|
225
|
+
return { files, messages };
|
|
226
|
+
}
|
|
227
|
+
// ── Generate default config ──
|
|
228
|
+
function generateDefaultConfig(overrides) {
|
|
229
|
+
return {
|
|
230
|
+
version: "1.0",
|
|
231
|
+
scan: {
|
|
232
|
+
failOn: "critical",
|
|
233
|
+
...overrides?.scan,
|
|
234
|
+
},
|
|
235
|
+
schedule: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
preset: "weekly",
|
|
238
|
+
cron: PRESETS.weekly,
|
|
239
|
+
timezone: "UTC",
|
|
240
|
+
...overrides?.schedule,
|
|
241
|
+
},
|
|
242
|
+
notify: {
|
|
243
|
+
onEvents: ["scan-complete", "score-drop", "critical-found", "fix-failed"],
|
|
244
|
+
...overrides?.notify,
|
|
245
|
+
},
|
|
246
|
+
fix: {
|
|
247
|
+
autoApply: false,
|
|
248
|
+
dryRunFirst: true,
|
|
249
|
+
requireApproval: ["pii-encrypt", "rls", "secrets"],
|
|
250
|
+
...overrides?.fix,
|
|
251
|
+
},
|
|
252
|
+
...overrides,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export type FixerCategory = "pii-encrypt" | "rls" | "secrets" | "routes-auth" | "headers" | "csrf" | "rate-limit" | "gitignore";
|
|
2
|
+
export type FixActionType = "sql-migration" | "file-patch" | "file-create" | "file-append" | "env-add" | "config-update" | "command" | "manual";
|
|
3
|
+
export type FixSeverity = "critical" | "high" | "medium" | "low";
|
|
4
|
+
export type FixStatus = "planned" | "previewed" | "applying" | "applied" | "verified" | "failed" | "skipped" | "manual-required";
|
|
5
|
+
export interface FixAction {
|
|
6
|
+
id: string;
|
|
7
|
+
type: FixActionType;
|
|
8
|
+
description: string;
|
|
9
|
+
descriptionHe: string;
|
|
10
|
+
severity: FixSeverity;
|
|
11
|
+
target: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
content: string;
|
|
14
|
+
rollback?: string;
|
|
15
|
+
status: FixStatus;
|
|
16
|
+
appliedAt?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
beforeHash?: string;
|
|
19
|
+
afterHash?: string;
|
|
20
|
+
dependsOn?: string[];
|
|
21
|
+
blockedBy?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface FixPlan {
|
|
24
|
+
fixer: FixerCategory;
|
|
25
|
+
name: string;
|
|
26
|
+
nameHe: string;
|
|
27
|
+
description: string;
|
|
28
|
+
descriptionHe: string;
|
|
29
|
+
actions: FixAction[];
|
|
30
|
+
totalActions: number;
|
|
31
|
+
autoFixable: number;
|
|
32
|
+
manualRequired: number;
|
|
33
|
+
estimatedScoreImpact: number;
|
|
34
|
+
affectedControls: string[];
|
|
35
|
+
prerequisites: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface FixerResult {
|
|
38
|
+
fixer: FixerCategory;
|
|
39
|
+
plan: FixPlan;
|
|
40
|
+
startedAt: string;
|
|
41
|
+
completedAt: string;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
applied: number;
|
|
44
|
+
failed: number;
|
|
45
|
+
skipped: number;
|
|
46
|
+
manualPending: number;
|
|
47
|
+
proofHash: string;
|
|
48
|
+
proofFile?: string;
|
|
49
|
+
scoreBefore: number;
|
|
50
|
+
scoreAfter: number;
|
|
51
|
+
}
|
|
52
|
+
export interface CapsuleConfig {
|
|
53
|
+
version: "1.0";
|
|
54
|
+
licenseKey?: string;
|
|
55
|
+
projectDir?: string;
|
|
56
|
+
scan: {
|
|
57
|
+
dbUrl?: string;
|
|
58
|
+
skipDirs?: string[];
|
|
59
|
+
failOn?: "critical" | "high" | "medium";
|
|
60
|
+
};
|
|
61
|
+
schedule?: {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
cron?: string;
|
|
64
|
+
preset?: "daily" | "weekly" | "monthly";
|
|
65
|
+
timezone?: string;
|
|
66
|
+
};
|
|
67
|
+
notify?: {
|
|
68
|
+
email?: string[];
|
|
69
|
+
webhook?: string[];
|
|
70
|
+
slack?: {
|
|
71
|
+
webhookUrl: string;
|
|
72
|
+
channel?: string;
|
|
73
|
+
mentionOn?: "critical" | "drop";
|
|
74
|
+
};
|
|
75
|
+
telegram?: {
|
|
76
|
+
botToken: string;
|
|
77
|
+
chatId: string;
|
|
78
|
+
};
|
|
79
|
+
onEvents: NotifyEvent[];
|
|
80
|
+
};
|
|
81
|
+
fix?: {
|
|
82
|
+
autoApply?: boolean;
|
|
83
|
+
dryRunFirst?: boolean;
|
|
84
|
+
skipFixers?: FixerCategory[];
|
|
85
|
+
requireApproval?: FixerCategory[];
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export type NotifyEvent = "scan-complete" | "score-drop" | "score-improve" | "critical-found" | "fix-applied" | "fix-failed" | "schedule-missed";
|
|
89
|
+
export interface NotifyPayload {
|
|
90
|
+
event: NotifyEvent;
|
|
91
|
+
timestamp: string;
|
|
92
|
+
projectName: string;
|
|
93
|
+
projectHash: string;
|
|
94
|
+
score: number;
|
|
95
|
+
previousScore: number | null;
|
|
96
|
+
delta: number;
|
|
97
|
+
critical: number;
|
|
98
|
+
high: number;
|
|
99
|
+
medium: number;
|
|
100
|
+
fixesApplied?: number;
|
|
101
|
+
fixesFailed?: number;
|
|
102
|
+
dashboardUrl?: string;
|
|
103
|
+
scanId: string;
|
|
104
|
+
proofHash: string;
|
|
105
|
+
}
|
|
106
|
+
export interface Fixer {
|
|
107
|
+
category: FixerCategory;
|
|
108
|
+
name: string;
|
|
109
|
+
nameHe: string;
|
|
110
|
+
/** Analyze scan results and generate a fix plan */
|
|
111
|
+
analyze(context: FixerContext): Promise<FixPlan>;
|
|
112
|
+
/** Apply the fix plan (or subset of actions) */
|
|
113
|
+
apply(plan: FixPlan, actionIds?: string[]): Promise<FixerResult>;
|
|
114
|
+
/** Verify fixes were applied correctly (re-scan specific controls) */
|
|
115
|
+
verify(result: FixerResult): Promise<boolean>;
|
|
116
|
+
}
|
|
117
|
+
export interface FixerContext {
|
|
118
|
+
projectDir: string;
|
|
119
|
+
dbUrl?: string;
|
|
120
|
+
config: CapsuleConfig;
|
|
121
|
+
scanResults: {
|
|
122
|
+
piiFields: Array<{
|
|
123
|
+
table: string;
|
|
124
|
+
column: string;
|
|
125
|
+
pii_type: string;
|
|
126
|
+
encrypted: boolean;
|
|
127
|
+
encryption_pattern: string | null;
|
|
128
|
+
}>;
|
|
129
|
+
routes: Array<{
|
|
130
|
+
path: string;
|
|
131
|
+
has_auth: boolean;
|
|
132
|
+
auth_type: string | null;
|
|
133
|
+
}>;
|
|
134
|
+
secrets: Array<{
|
|
135
|
+
file: string;
|
|
136
|
+
line: number;
|
|
137
|
+
type: string;
|
|
138
|
+
severity: string;
|
|
139
|
+
is_env_interpolated: boolean;
|
|
140
|
+
}>;
|
|
141
|
+
rls: Array<{
|
|
142
|
+
table: string;
|
|
143
|
+
rls_enabled: boolean;
|
|
144
|
+
policy_count: number;
|
|
145
|
+
}>;
|
|
146
|
+
framework: string;
|
|
147
|
+
database: string;
|
|
148
|
+
};
|
|
149
|
+
stack: {
|
|
150
|
+
framework: string;
|
|
151
|
+
database: string;
|
|
152
|
+
language: string;
|
|
153
|
+
hosting: string;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export interface CITemplate {
|
|
157
|
+
provider: "github-actions" | "gitlab-ci" | "bitbucket";
|
|
158
|
+
filename: string;
|
|
159
|
+
content: string;
|
|
160
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════
|
|
3
|
+
// Guard Capsule — Fixer Types
|
|
4
|
+
//
|
|
5
|
+
// Every fixer follows the same pattern:
|
|
6
|
+
// analyze → plan → preview → apply → verify
|
|
7
|
+
//
|
|
8
|
+
// Fixers NEVER send data externally.
|
|
9
|
+
// Fixers ALWAYS produce a proof of what changed.
|
|
10
|
+
// ═══════════════════════════════════════════════════════════
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|