cipher-security 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/bin/cipher.js +566 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +991 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +288 -0
- package/package.json +31 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Leaderboard → Researcher Feedback Loop.
|
|
7
|
+
*
|
|
8
|
+
* Connects skill performance metrics to autonomous improvement:
|
|
9
|
+
* - Identifies bottom-performing skills from leaderboard
|
|
10
|
+
* - Triggers skill regeneration via quality analysis
|
|
11
|
+
* - Tracks improvement cycles and validates quality gains
|
|
12
|
+
*
|
|
13
|
+
* @module autonomous/feedback-loop
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, chmodSync } from 'node:fs';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// ImprovementStatus
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export const ImprovementStatus = Object.freeze({
|
|
25
|
+
IDENTIFIED: 'identified',
|
|
26
|
+
ANALYZING: 'analyzing',
|
|
27
|
+
REGENERATING: 'regenerating',
|
|
28
|
+
VALIDATING: 'validating',
|
|
29
|
+
COMPLETED: 'completed',
|
|
30
|
+
FAILED: 'failed',
|
|
31
|
+
SKIPPED: 'skipped',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// ImprovementCandidate
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export class ImprovementCandidate {
|
|
39
|
+
constructor({
|
|
40
|
+
skillPath = '',
|
|
41
|
+
domain = '',
|
|
42
|
+
currentScore = 0,
|
|
43
|
+
invocations = 0,
|
|
44
|
+
trend = '',
|
|
45
|
+
issues = [],
|
|
46
|
+
status = ImprovementStatus.IDENTIFIED,
|
|
47
|
+
improvementScore = 0,
|
|
48
|
+
cycleId = '',
|
|
49
|
+
startedAt = 0,
|
|
50
|
+
completedAt = 0,
|
|
51
|
+
} = {}) {
|
|
52
|
+
this.skillPath = skillPath;
|
|
53
|
+
this.domain = domain;
|
|
54
|
+
this.currentScore = currentScore;
|
|
55
|
+
this.invocations = invocations;
|
|
56
|
+
this.trend = trend;
|
|
57
|
+
this.issues = [...issues];
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.improvementScore = improvementScore;
|
|
60
|
+
this.cycleId = cycleId;
|
|
61
|
+
this.startedAt = startedAt;
|
|
62
|
+
this.completedAt = completedAt;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// ImprovementCycle
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export class ImprovementCycle {
|
|
71
|
+
constructor({
|
|
72
|
+
cycleId = '',
|
|
73
|
+
startedAt = 0,
|
|
74
|
+
candidates = [],
|
|
75
|
+
completedAt = 0,
|
|
76
|
+
skillsImproved = 0,
|
|
77
|
+
skillsFailed = 0,
|
|
78
|
+
skillsSkipped = 0,
|
|
79
|
+
avgScoreBefore = 0,
|
|
80
|
+
avgScoreAfter = 0,
|
|
81
|
+
} = {}) {
|
|
82
|
+
this.cycleId = cycleId;
|
|
83
|
+
this.startedAt = startedAt;
|
|
84
|
+
this.candidates = candidates;
|
|
85
|
+
this.completedAt = completedAt;
|
|
86
|
+
this.skillsImproved = skillsImproved;
|
|
87
|
+
this.skillsFailed = skillsFailed;
|
|
88
|
+
this.skillsSkipped = skillsSkipped;
|
|
89
|
+
this.avgScoreBefore = avgScoreBefore;
|
|
90
|
+
this.avgScoreAfter = avgScoreAfter;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// SkillQualityAnalyzer
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export class SkillQualityAnalyzer {
|
|
99
|
+
static MIN_SKILL_MD_LINES = 20;
|
|
100
|
+
static MIN_AGENT_PY_LINES = 15;
|
|
101
|
+
static MIN_CODE_BLOCKS = 1;
|
|
102
|
+
static REQUIRED_SECTIONS = new Set(['overview', 'techniques', 'best practices', 'references']);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} [skillsDir='skills']
|
|
106
|
+
*/
|
|
107
|
+
constructor(skillsDir = 'skills') {
|
|
108
|
+
this.skillsDir = resolve(String(skillsDir));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Analyze a skill's quality and return findings.
|
|
113
|
+
* @param {string} skillPath
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
analyzeSkill(skillPath) {
|
|
117
|
+
let fullPath;
|
|
118
|
+
if (isAbsolute(skillPath)) {
|
|
119
|
+
fullPath = skillPath;
|
|
120
|
+
} else {
|
|
121
|
+
// Try relative to parent of skillsDir
|
|
122
|
+
fullPath = resolve(join(this.skillsDir, '..'), skillPath);
|
|
123
|
+
if (!existsSync(fullPath)) {
|
|
124
|
+
const cleaned = skillPath.startsWith('skills/') ? skillPath.slice(7) : skillPath;
|
|
125
|
+
fullPath = join(this.skillsDir, cleaned);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const issues = [];
|
|
130
|
+
const scores = {};
|
|
131
|
+
|
|
132
|
+
// Check SKILL.md
|
|
133
|
+
const skillMd = join(fullPath, 'SKILL.md');
|
|
134
|
+
if (existsSync(skillMd)) {
|
|
135
|
+
const content = readFileSync(skillMd, 'utf-8');
|
|
136
|
+
const lines = content.trim().split('\n');
|
|
137
|
+
|
|
138
|
+
if (lines.length < SkillQualityAnalyzer.MIN_SKILL_MD_LINES) {
|
|
139
|
+
issues.push(`SKILL.md too short (${lines.length} lines, min ${SkillQualityAnalyzer.MIN_SKILL_MD_LINES})`);
|
|
140
|
+
}
|
|
141
|
+
scores.content_length = Math.min(lines.length / 50, 1.0);
|
|
142
|
+
|
|
143
|
+
const lower = content.toLowerCase();
|
|
144
|
+
const foundSections = new Set();
|
|
145
|
+
for (const s of SkillQualityAnalyzer.REQUIRED_SECTIONS) {
|
|
146
|
+
if (lower.includes(`## ${s}`) || lower.includes(`# ${s}`)) {
|
|
147
|
+
foundSections.add(s);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const missing = [...SkillQualityAnalyzer.REQUIRED_SECTIONS].filter(s => !foundSections.has(s));
|
|
151
|
+
if (missing.length > 0) {
|
|
152
|
+
issues.push(`Missing sections: ${missing.sort().join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
scores.section_coverage = foundSections.size / SkillQualityAnalyzer.REQUIRED_SECTIONS.size;
|
|
155
|
+
|
|
156
|
+
const codeBlocks = (content.match(/```/g) || []).length;
|
|
157
|
+
if (codeBlocks < SkillQualityAnalyzer.MIN_CODE_BLOCKS * 2) {
|
|
158
|
+
issues.push('No code blocks found');
|
|
159
|
+
}
|
|
160
|
+
scores.code_examples = Math.min(codeBlocks / 6, 1.0);
|
|
161
|
+
|
|
162
|
+
if (!content.slice(0, 200).includes('Copyright')) {
|
|
163
|
+
issues.push('Missing copyright header');
|
|
164
|
+
}
|
|
165
|
+
scores.frontmatter = content.slice(0, 10).includes('---') ? 1.0 : 0.0;
|
|
166
|
+
if (!content.slice(0, 10).includes('---')) {
|
|
167
|
+
issues.push('Missing YAML frontmatter');
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
issues.push('SKILL.md missing');
|
|
171
|
+
scores.content_length = 0;
|
|
172
|
+
scores.section_coverage = 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check agent.py
|
|
176
|
+
const agentPy = join(fullPath, 'scripts', 'agent.py');
|
|
177
|
+
if (existsSync(agentPy)) {
|
|
178
|
+
const agentContent = readFileSync(agentPy, 'utf-8');
|
|
179
|
+
const agentLines = agentContent.trim().split('\n');
|
|
180
|
+
if (agentLines.length < SkillQualityAnalyzer.MIN_AGENT_PY_LINES) {
|
|
181
|
+
issues.push(`agent.py too short (${agentLines.length} lines)`);
|
|
182
|
+
}
|
|
183
|
+
scores.agent_quality = Math.min(agentLines.length / 50, 1.0);
|
|
184
|
+
if (!agentContent.includes('argparse')) issues.push('agent.py missing argparse');
|
|
185
|
+
if (!agentContent.includes('json')) issues.push('agent.py missing JSON output');
|
|
186
|
+
if (!agentContent.includes('__main__')) issues.push('agent.py missing __main__ guard');
|
|
187
|
+
} else {
|
|
188
|
+
issues.push('scripts/agent.py missing');
|
|
189
|
+
scores.agent_quality = 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check references
|
|
193
|
+
const refDir = join(fullPath, 'references');
|
|
194
|
+
if (existsSync(refDir)) {
|
|
195
|
+
try {
|
|
196
|
+
const refs = readdirSync(refDir).filter(f => f.endsWith('.md'));
|
|
197
|
+
scores.references = refs.length > 0 ? 1.0 : 0.0;
|
|
198
|
+
if (refs.length === 0) issues.push('No reference documentation');
|
|
199
|
+
} catch {
|
|
200
|
+
scores.references = 0;
|
|
201
|
+
issues.push('No reference documentation');
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
issues.push('No reference documentation');
|
|
205
|
+
scores.references = 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const vals = Object.values(scores);
|
|
209
|
+
const overall = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
path: fullPath,
|
|
213
|
+
issues,
|
|
214
|
+
scores,
|
|
215
|
+
overallQuality: Math.round(overall * 1000) / 1000,
|
|
216
|
+
needsImprovement: issues.length > 2 || overall < 0.6,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Helper to check if a path is absolute. */
|
|
222
|
+
function isAbsolute(p) {
|
|
223
|
+
return p.startsWith('/');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// FeedbackLoop
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
export class FeedbackLoop {
|
|
231
|
+
static DEFAULT_BATCH_SIZE = 10;
|
|
232
|
+
static MIN_INVOCATIONS_FOR_FEEDBACK = 1;
|
|
233
|
+
static SCORE_THRESHOLD = 0.5;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {object} [opts]
|
|
237
|
+
* @param {string} [opts.skillsDir='skills']
|
|
238
|
+
* @param {number} [opts.maxImprovementsPerCycle=10]
|
|
239
|
+
*/
|
|
240
|
+
constructor({ skillsDir = 'skills', maxImprovementsPerCycle = 10 } = {}) {
|
|
241
|
+
this.skillsDir = resolve(String(skillsDir));
|
|
242
|
+
this.maxPerCycle = maxImprovementsPerCycle;
|
|
243
|
+
this.analyzer = new SkillQualityAnalyzer(this.skillsDir);
|
|
244
|
+
/** @type {ImprovementCycle[]} */
|
|
245
|
+
this._history = [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Identify skills that need improvement.
|
|
250
|
+
* @param {object} [opts]
|
|
251
|
+
* @param {object[]|null} [opts.bottomSkills]
|
|
252
|
+
* @param {number} [opts.scoreThreshold]
|
|
253
|
+
* @returns {ImprovementCandidate[]}
|
|
254
|
+
*/
|
|
255
|
+
identifyCandidates({ bottomSkills = null, scoreThreshold } = {}) {
|
|
256
|
+
const threshold = scoreThreshold ?? FeedbackLoop.SCORE_THRESHOLD;
|
|
257
|
+
const candidates = [];
|
|
258
|
+
|
|
259
|
+
if (!bottomSkills) {
|
|
260
|
+
return this._identifyByQuality();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const skill of bottomSkills) {
|
|
264
|
+
const path = skill.skillPath || skill.path || '';
|
|
265
|
+
const score = skill.score ?? 0;
|
|
266
|
+
const invocations = skill.invocations ?? 0;
|
|
267
|
+
const trend = skill.trend ?? 'stable';
|
|
268
|
+
|
|
269
|
+
if (score >= threshold && trend !== 'declining') continue;
|
|
270
|
+
|
|
271
|
+
const analysis = this.analyzer.analyzeSkill(path);
|
|
272
|
+
candidates.push(new ImprovementCandidate({
|
|
273
|
+
skillPath: path,
|
|
274
|
+
domain: FeedbackLoop._extractDomain(path),
|
|
275
|
+
currentScore: score,
|
|
276
|
+
invocations,
|
|
277
|
+
trend,
|
|
278
|
+
issues: analysis.issues || [],
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Sort: declining first, then lowest score, then most used
|
|
283
|
+
candidates.sort((a, b) => {
|
|
284
|
+
const aDecl = a.trend === 'declining' ? 1 : 0;
|
|
285
|
+
const bDecl = b.trend === 'declining' ? 1 : 0;
|
|
286
|
+
if (bDecl !== aDecl) return bDecl - aDecl;
|
|
287
|
+
if (a.currentScore !== b.currentScore) return a.currentScore - b.currentScore;
|
|
288
|
+
return b.invocations - a.invocations;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return candidates.slice(0, this.maxPerCycle);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @private */
|
|
295
|
+
_identifyByQuality() {
|
|
296
|
+
const candidates = [];
|
|
297
|
+
if (!existsSync(this.skillsDir)) return candidates;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
for (const domainName of readdirSync(this.skillsDir).sort()) {
|
|
301
|
+
const domainDir = join(this.skillsDir, domainName);
|
|
302
|
+
if (domainName.startsWith('.') || !statSync(domainDir).isDirectory()) continue;
|
|
303
|
+
const techniquesDir = join(domainDir, 'techniques');
|
|
304
|
+
if (!existsSync(techniquesDir)) continue;
|
|
305
|
+
|
|
306
|
+
for (const techName of readdirSync(techniquesDir).sort()) {
|
|
307
|
+
const techDir = join(techniquesDir, techName);
|
|
308
|
+
if (!statSync(techDir).isDirectory()) continue;
|
|
309
|
+
const relPath = `skills/${domainName}/techniques/${techName}`;
|
|
310
|
+
const analysis = this.analyzer.analyzeSkill(relPath);
|
|
311
|
+
if (analysis.needsImprovement) {
|
|
312
|
+
candidates.push(new ImprovementCandidate({
|
|
313
|
+
skillPath: relPath,
|
|
314
|
+
domain: domainName,
|
|
315
|
+
currentScore: analysis.overallQuality,
|
|
316
|
+
invocations: 0,
|
|
317
|
+
trend: 'unknown',
|
|
318
|
+
issues: analysis.issues,
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch { /* ok */ }
|
|
324
|
+
|
|
325
|
+
candidates.sort((a, b) => a.currentScore - b.currentScore);
|
|
326
|
+
return candidates.slice(0, this.maxPerCycle);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run a full improvement cycle.
|
|
331
|
+
* @param {object} [opts]
|
|
332
|
+
* @param {ImprovementCandidate[]|null} [opts.candidates]
|
|
333
|
+
* @param {boolean} [opts.dryRun=false]
|
|
334
|
+
* @returns {ImprovementCycle}
|
|
335
|
+
*/
|
|
336
|
+
runImprovementCycle({ candidates = null, dryRun = false } = {}) {
|
|
337
|
+
const cycleId = `cycle-${randomBytes(4).toString('hex')}`;
|
|
338
|
+
const cycle = new ImprovementCycle({
|
|
339
|
+
cycleId,
|
|
340
|
+
startedAt: Date.now() / 1000,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!candidates) {
|
|
344
|
+
candidates = this.identifyCandidates();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
cycle.candidates = candidates;
|
|
348
|
+
const scoresBefore = [];
|
|
349
|
+
|
|
350
|
+
for (const candidate of candidates) {
|
|
351
|
+
candidate.cycleId = cycleId;
|
|
352
|
+
candidate.startedAt = Date.now() / 1000;
|
|
353
|
+
scoresBefore.push(candidate.currentScore);
|
|
354
|
+
|
|
355
|
+
if (dryRun) {
|
|
356
|
+
candidate.status = ImprovementStatus.SKIPPED;
|
|
357
|
+
cycle.skillsSkipped += 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
candidate.status = ImprovementStatus.ANALYZING;
|
|
363
|
+
const analysis = this.analyzer.analyzeSkill(candidate.skillPath);
|
|
364
|
+
|
|
365
|
+
if (!analysis.needsImprovement) {
|
|
366
|
+
candidate.status = ImprovementStatus.SKIPPED;
|
|
367
|
+
cycle.skillsSkipped += 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
candidate.status = ImprovementStatus.REGENERATING;
|
|
372
|
+
const improved = this._improveSkill(candidate, analysis);
|
|
373
|
+
|
|
374
|
+
if (improved) {
|
|
375
|
+
candidate.status = ImprovementStatus.VALIDATING;
|
|
376
|
+
const validation = this.analyzer.analyzeSkill(candidate.skillPath);
|
|
377
|
+
candidate.improvementScore = validation.overallQuality;
|
|
378
|
+
candidate.status = ImprovementStatus.COMPLETED;
|
|
379
|
+
cycle.skillsImproved += 1;
|
|
380
|
+
} else {
|
|
381
|
+
candidate.status = ImprovementStatus.FAILED;
|
|
382
|
+
cycle.skillsFailed += 1;
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
candidate.status = ImprovementStatus.FAILED;
|
|
386
|
+
cycle.skillsFailed += 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
candidate.completedAt = Date.now() / 1000;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
cycle.completedAt = Date.now() / 1000;
|
|
393
|
+
cycle.avgScoreBefore = scoresBefore.length > 0
|
|
394
|
+
? scoresBefore.reduce((a, b) => a + b, 0) / scoresBefore.length : 0;
|
|
395
|
+
const scoresAfter = candidates
|
|
396
|
+
.filter(c => c.improvementScore > 0)
|
|
397
|
+
.map(c => c.improvementScore);
|
|
398
|
+
cycle.avgScoreAfter = scoresAfter.length > 0
|
|
399
|
+
? scoresAfter.reduce((a, b) => a + b, 0) / scoresAfter.length : 0;
|
|
400
|
+
|
|
401
|
+
this._history.push(cycle);
|
|
402
|
+
return cycle;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Improve a single skill based on analysis findings.
|
|
407
|
+
* @private
|
|
408
|
+
* @param {ImprovementCandidate} candidate
|
|
409
|
+
* @param {object} analysis
|
|
410
|
+
* @returns {boolean}
|
|
411
|
+
*/
|
|
412
|
+
_improveSkill(candidate, analysis) {
|
|
413
|
+
let skillDir = candidate.skillPath;
|
|
414
|
+
if (!isAbsolute(skillDir)) {
|
|
415
|
+
skillDir = resolve(join(this.skillsDir, '..'), candidate.skillPath);
|
|
416
|
+
}
|
|
417
|
+
if (!existsSync(skillDir)) return false;
|
|
418
|
+
|
|
419
|
+
let improved = false;
|
|
420
|
+
|
|
421
|
+
// Fix missing sections in SKILL.md
|
|
422
|
+
const skillMd = join(skillDir, 'SKILL.md');
|
|
423
|
+
if (existsSync(skillMd)) {
|
|
424
|
+
let content = readFileSync(skillMd, 'utf-8');
|
|
425
|
+
const lower = content.toLowerCase();
|
|
426
|
+
const additions = [];
|
|
427
|
+
|
|
428
|
+
if (!lower.includes('## overview') && !lower.includes('# overview')) {
|
|
429
|
+
additions.push('\n## Overview\nThis technique provides security analysis capabilities.\n');
|
|
430
|
+
}
|
|
431
|
+
if (!lower.includes('## techniques') && !lower.includes('# techniques')) {
|
|
432
|
+
additions.push('\n## Techniques\n- Analysis and detection methods\n- Tool configuration and usage\n- Threat indicator identification\n');
|
|
433
|
+
}
|
|
434
|
+
if (!lower.includes('## best practices') && !lower.includes('# best practices')) {
|
|
435
|
+
additions.push('\n## Best Practices\n- Follow defense-in-depth principles\n- Validate against known-good baselines\n- Document all findings with evidence\n');
|
|
436
|
+
}
|
|
437
|
+
if (!lower.includes('## references') && !lower.includes('# references')) {
|
|
438
|
+
additions.push('\n## References\n- MITRE ATT&CK Framework\n- CIS Controls v8\n- NIST SP 800-53 Rev. 5\n');
|
|
439
|
+
}
|
|
440
|
+
if (!content.includes('```')) {
|
|
441
|
+
additions.push('\n## Example\n\n```yaml\n# Configuration example\nenabled: true\nlevel: high\n```\n');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (additions.length > 0) {
|
|
445
|
+
content += additions.join('');
|
|
446
|
+
writeFileSync(skillMd, content, 'utf-8');
|
|
447
|
+
improved = true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return improved;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Generate a report for an improvement cycle.
|
|
456
|
+
* @param {ImprovementCycle} cycle
|
|
457
|
+
* @returns {object}
|
|
458
|
+
*/
|
|
459
|
+
getCycleReport(cycle) {
|
|
460
|
+
return {
|
|
461
|
+
cycleId: cycle.cycleId,
|
|
462
|
+
durationS: cycle.completedAt
|
|
463
|
+
? Math.round((cycle.completedAt - cycle.startedAt) * 100) / 100
|
|
464
|
+
: 0,
|
|
465
|
+
totalCandidates: cycle.candidates.length,
|
|
466
|
+
improved: cycle.skillsImproved,
|
|
467
|
+
failed: cycle.skillsFailed,
|
|
468
|
+
skipped: cycle.skillsSkipped,
|
|
469
|
+
avgScoreBefore: Math.round(cycle.avgScoreBefore * 1000) / 1000,
|
|
470
|
+
avgScoreAfter: Math.round(cycle.avgScoreAfter * 1000) / 1000,
|
|
471
|
+
candidates: cycle.candidates.map(c => ({
|
|
472
|
+
path: c.skillPath,
|
|
473
|
+
domain: c.domain,
|
|
474
|
+
status: c.status,
|
|
475
|
+
scoreBefore: c.currentScore,
|
|
476
|
+
scoreAfter: c.improvementScore,
|
|
477
|
+
issues: c.issues,
|
|
478
|
+
})),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Return all cycle reports. */
|
|
483
|
+
getHistory() {
|
|
484
|
+
return this._history.map(c => this.getCycleReport(c));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Extract domain name from skill path.
|
|
489
|
+
* @param {string} skillPath
|
|
490
|
+
* @returns {string}
|
|
491
|
+
*/
|
|
492
|
+
static _extractDomain(skillPath) {
|
|
493
|
+
const parts = skillPath.split('/').filter(Boolean);
|
|
494
|
+
for (let i = 0; i < parts.length; i++) {
|
|
495
|
+
if (parts[i] === 'skills' && i + 1 < parts.length) {
|
|
496
|
+
return parts[i + 1];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (parts.length >= 2) {
|
|
500
|
+
return parts[0] === 'skills' ? parts[1] : parts[0];
|
|
501
|
+
}
|
|
502
|
+
return 'unknown';
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// Convenience function
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Run a complete feedback cycle: leaderboard → analysis → improvement.
|
|
512
|
+
* @param {object} [opts]
|
|
513
|
+
* @param {string} [opts.skillsDir='skills']
|
|
514
|
+
* @param {number} [opts.maxImprovements=10]
|
|
515
|
+
* @param {number} [opts.scoreThreshold=0.5]
|
|
516
|
+
* @param {boolean} [opts.dryRun=false]
|
|
517
|
+
* @returns {object}
|
|
518
|
+
*/
|
|
519
|
+
export function connectLeaderboardToResearcher({
|
|
520
|
+
skillsDir = 'skills',
|
|
521
|
+
maxImprovements = 10,
|
|
522
|
+
scoreThreshold = 0.5,
|
|
523
|
+
dryRun = false,
|
|
524
|
+
} = {}) {
|
|
525
|
+
const loop = new FeedbackLoop({
|
|
526
|
+
skillsDir,
|
|
527
|
+
maxImprovementsPerCycle: maxImprovements,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Try to get leaderboard data
|
|
531
|
+
let bottomSkills = null;
|
|
532
|
+
try {
|
|
533
|
+
const { SkillLeaderboard } = require('./leaderboard.js');
|
|
534
|
+
const lb = new SkillLeaderboard();
|
|
535
|
+
const bottomEntries = lb.getBottomSkills(maxImprovements * 2);
|
|
536
|
+
bottomSkills = bottomEntries.map(e => ({
|
|
537
|
+
skillPath: e.skillPath,
|
|
538
|
+
score: e.score,
|
|
539
|
+
invocations: e.invocations,
|
|
540
|
+
trend: e.trend,
|
|
541
|
+
}));
|
|
542
|
+
lb.close();
|
|
543
|
+
} catch {
|
|
544
|
+
// Leaderboard not available — quality-only analysis
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const candidates = loop.identifyCandidates({
|
|
548
|
+
bottomSkills,
|
|
549
|
+
scoreThreshold,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const cycle = loop.runImprovementCycle({ candidates, dryRun });
|
|
553
|
+
return loop.getCycleReport(cycle);
|
|
554
|
+
}
|