ship-safe 6.4.0 → 8.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.
@@ -50,6 +50,72 @@ const AGENTIC_MAP = {
50
50
  'ASI10': { soc2: ['CC7.2', 'CC7.4'], iso27001: ['A.8.9', 'A.5.30'], nistAiRmf: ['MANAGE 2.2', 'MANAGE 4.1'] },
51
51
  };
52
52
 
53
+ // =============================================================================
54
+ // OWASP AGENTIC AI TOP 10 (December 2025)
55
+ // =============================================================================
56
+
57
+ const OWASP_AGENTIC_TOP_10 = {
58
+ ASI01: { id: 'ASI01', title: 'Agent Goal Hijacking', description: 'Manipulation of agent objectives through prompt injection, memory poisoning, or instruction override.' },
59
+ ASI02: { id: 'ASI02', title: 'Tool Misuse', description: 'Agent uses tools in unintended or dangerous ways — shell execution, file deletion, network access beyond scope.' },
60
+ ASI03: { id: 'ASI03', title: 'Privilege Abuse', description: 'Agent operates with excessive permissions — writes outside project, accesses secrets, escalates access.' },
61
+ ASI04: { id: 'ASI04', title: 'Agentic Supply Chain', description: 'Compromised skills, MCP servers, or tool packages that the agent depends on.' },
62
+ ASI05: { id: 'ASI05', title: 'Memory & Context Poisoning', description: 'Malicious data persisted in agent memory, rules files, or context that survives sessions.' },
63
+ ASI06: { id: 'ASI06', title: 'Uncontrolled Data Exposure', description: 'Agent leaks code, secrets, or PII through tool outputs, logs, or external API calls.' },
64
+ ASI07: { id: 'ASI07', title: 'Insecure Communication', description: 'Unencrypted MCP transport, HTTP model endpoints, or plaintext inter-agent messaging.' },
65
+ ASI08: { id: 'ASI08', title: 'Missing Human Oversight', description: 'Agent takes destructive or irreversible actions without user confirmation — proactive mode risks.' },
66
+ ASI09: { id: 'ASI09', title: 'Weak Identity & Auth', description: 'Agent sessions without authentication, shared API keys, or no audit trail of actions.' },
67
+ ASI10: { id: 'ASI10', title: 'Rogue Agent Behavior', description: 'Agent deviates from intended behavior — self-modification, stealth mode, output suppression.' },
68
+ };
69
+
70
+ /**
71
+ * Enrich a finding with OWASP Agentic Top 10 metadata.
72
+ * Attaches `agenticRisk` object if the finding maps to ASI01–ASI10.
73
+ * @param {object} finding
74
+ * @returns {object} — finding with agenticRisk attached (or unchanged)
75
+ */
76
+ export function enrichAgenticRisk(finding) {
77
+ const owasp = finding.owasp;
78
+ if (!owasp || !OWASP_AGENTIC_TOP_10[owasp]) return finding;
79
+
80
+ const risk = OWASP_AGENTIC_TOP_10[owasp];
81
+ finding.agenticRisk = {
82
+ id: risk.id,
83
+ title: risk.title,
84
+ description: risk.description,
85
+ };
86
+ return finding;
87
+ }
88
+
89
+ /**
90
+ * Get OWASP Agentic Top 10 summary across all findings.
91
+ * @param {object[]} findings
92
+ * @returns {{ risks: object[], coverage: number }}
93
+ */
94
+ export function getAgenticSummary(findings) {
95
+ const counts = {};
96
+ for (const f of findings) {
97
+ const owasp = f.owasp;
98
+ if (owasp && OWASP_AGENTIC_TOP_10[owasp]) {
99
+ counts[owasp] = (counts[owasp] || 0) + 1;
100
+ }
101
+ }
102
+
103
+ const risks = Object.entries(OWASP_AGENTIC_TOP_10).map(([id, info]) => ({
104
+ ...info,
105
+ findingCount: counts[id] || 0,
106
+ status: counts[id] ? 'flagged' : 'clear',
107
+ }));
108
+
109
+ const flagged = risks.filter(r => r.findingCount > 0).length;
110
+
111
+ return {
112
+ risks,
113
+ flagged,
114
+ total: 10,
115
+ coverage: `${flagged}/10`,
116
+ };
117
+ }
118
+
53
119
  // =============================================================================
54
120
  // PUBLIC API
55
121
  // =============================================================================
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Hermes Tool Registry — Ship Safe × Hermes Agent
3
+ * =================================================
4
+ *
5
+ * Declares Ship Safe's five security tools in the Hermes Agent tool-registry
6
+ * format. Import this module in your Hermes agent bootstrap to register
7
+ * Ship Safe as a first-class citizen in the tool registry.
8
+ *
9
+ * USAGE:
10
+ * import { HERMES_TOOLS, registerWithHermes } from './hermes-tool-registry.js';
11
+ *
12
+ * // Option A — register all tools at once
13
+ * await registerWithHermes(agent.toolRegistry);
14
+ *
15
+ * // Option B — use the raw definitions
16
+ * for (const tool of HERMES_TOOLS) agent.toolRegistry.register(tool);
17
+ *
18
+ * SECURITY NOTE:
19
+ * These definitions are pinned, hardcoded, and integrity-verified at load
20
+ * time. They are never fetched from a remote URL. Do not replace the
21
+ * INTEGRITY_HASH values without auditing the updated definitions.
22
+ */
23
+
24
+ import { createHash } from 'crypto';
25
+ import path from 'path';
26
+ import { fileURLToPath } from 'url';
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+
30
+ // =============================================================================
31
+ // TOOL DEFINITIONS (Hermes function-call schema format)
32
+ // =============================================================================
33
+
34
+ export const HERMES_TOOLS = [
35
+ {
36
+ name: 'ship_safe_audit',
37
+ description:
38
+ 'Run a Ship Safe security audit on a local codebase directory. ' +
39
+ 'Returns a findings report with severity-graded issues, CWE/OWASP mappings, ' +
40
+ 'and remediation guidance. Use before deploying any code or merging PRs.',
41
+ parameters: {
42
+ type: 'object',
43
+ properties: {
44
+ path: {
45
+ type: 'string',
46
+ description: 'Absolute path to the project root directory to scan.',
47
+ },
48
+ severity: {
49
+ type: 'string',
50
+ enum: ['critical', 'high', 'medium', 'low'],
51
+ description: 'Minimum severity threshold for reported findings.',
52
+ default: 'medium',
53
+ },
54
+ deep: {
55
+ type: 'boolean',
56
+ description: 'Enable deep LLM-powered taint analysis (Haiku→Sonnet→Opus pipeline). Slower but more accurate.',
57
+ default: false,
58
+ },
59
+ },
60
+ required: ['path'],
61
+ additionalProperties: false,
62
+ },
63
+ handler: async ({ path: scanPath, severity = 'medium', deep = false }) => {
64
+ const { auditCommand } = await import('../commands/audit.js');
65
+ return auditCommand(scanPath, { severity, deep, json: true, quiet: true });
66
+ },
67
+ },
68
+
69
+ {
70
+ name: 'ship_safe_scan_mcp',
71
+ description:
72
+ 'Analyze an MCP server manifest (URL or local file path) for security issues ' +
73
+ 'before connecting. Checks for prompt injection in tool descriptions, credential ' +
74
+ 'harvesting patterns, Hermes function-call poisoning, schema bypass (additionalProperties: true), ' +
75
+ 'and known-malicious server hashes. Returns per-tool findings.',
76
+ parameters: {
77
+ type: 'object',
78
+ properties: {
79
+ target: {
80
+ type: 'string',
81
+ description: 'URL (https://...) or absolute local file path to the MCP manifest JSON.',
82
+ },
83
+ },
84
+ required: ['target'],
85
+ additionalProperties: false,
86
+ },
87
+ handler: async ({ target }) => {
88
+ const { scanMcpCommand } = await import('../commands/scan-mcp.js');
89
+ return scanMcpCommand(target, { json: true });
90
+ },
91
+ },
92
+
93
+ {
94
+ name: 'ship_safe_get_findings',
95
+ description:
96
+ 'Retrieve findings from the last saved Ship Safe scan report for a project. ' +
97
+ 'Optionally filter by minimum severity. Returns an array of findings with rule, ' +
98
+ 'title, severity, file, line, and remediation guidance.',
99
+ parameters: {
100
+ type: 'object',
101
+ properties: {
102
+ path: {
103
+ type: 'string',
104
+ description: 'Absolute path to the project root (used to locate the saved report).',
105
+ },
106
+ severity: {
107
+ type: 'string',
108
+ enum: ['critical', 'high', 'medium', 'low'],
109
+ description: 'Minimum severity to include in results.',
110
+ default: 'medium',
111
+ },
112
+ },
113
+ required: ['path'],
114
+ additionalProperties: false,
115
+ },
116
+ handler: async ({ path: projectPath, severity = 'medium' }) => {
117
+ const { mcpGetFindings } = await import('../commands/mcp.js');
118
+ return mcpGetFindings({ projectPath, severity });
119
+ },
120
+ },
121
+
122
+ {
123
+ name: 'ship_safe_suppress_finding',
124
+ description:
125
+ 'Suppress a known-safe finding by inserting an inline ship-safe-ignore comment ' +
126
+ 'in the source file before the flagged line. Use only when the finding is a ' +
127
+ 'confirmed false positive and you can document why it is safe.',
128
+ parameters: {
129
+ type: 'object',
130
+ properties: {
131
+ file: {
132
+ type: 'string',
133
+ description: 'Absolute path to the source file containing the finding.',
134
+ },
135
+ line: {
136
+ type: 'number',
137
+ description: 'Line number of the finding (1-based).',
138
+ },
139
+ reason: {
140
+ type: 'string',
141
+ description: 'Human-readable explanation of why this finding is safe to suppress.',
142
+ },
143
+ },
144
+ required: ['file', 'line', 'reason'],
145
+ additionalProperties: false,
146
+ },
147
+ handler: async ({ file, line, reason }) => {
148
+ const { mcpSuppressFinding } = await import('../commands/mcp.js');
149
+ return mcpSuppressFinding({ file, line, reason });
150
+ },
151
+ },
152
+
153
+ {
154
+ name: 'ship_safe_memory_list',
155
+ description:
156
+ 'List all entries in the Ship Safe security memory for a project. ' +
157
+ 'The memory stores learned false positives that are automatically filtered ' +
158
+ 'from future scans. Returns each entry with its rule, file pattern, and ' +
159
+ 'the snippet that was suppressed.',
160
+ parameters: {
161
+ type: 'object',
162
+ properties: {
163
+ path: {
164
+ type: 'string',
165
+ description: 'Absolute path to the project root.',
166
+ },
167
+ },
168
+ required: ['path'],
169
+ additionalProperties: false,
170
+ },
171
+ handler: async ({ path: projectPath }) => {
172
+ const { SecurityMemory } = await import('./security-memory.js');
173
+ const mem = new SecurityMemory(projectPath);
174
+ return mem.list();
175
+ },
176
+ },
177
+ ];
178
+
179
+ // =============================================================================
180
+ // INTEGRITY VERIFICATION
181
+ // Each tool definition is hashed at module load time. If the registry is
182
+ // tampered with (e.g. supply-chain attack), the hash check will fail.
183
+ // Run `node -e "import('./hermes-tool-registry.js').then(m => m.printHashes())"` to regenerate.
184
+ // =============================================================================
185
+
186
+ const KNOWN_HASHES = {
187
+ ship_safe_audit: '4d282d29e44fcc01',
188
+ ship_safe_scan_mcp: 'f967aea9626ca840',
189
+ ship_safe_get_findings: 'c09c9447efd574b3',
190
+ ship_safe_suppress_finding: '3b7339419fe52ac7',
191
+ ship_safe_memory_list: 'c71c996716d1805b',
192
+ };
193
+
194
+ function toolHash(tool) {
195
+ // Hash name + description + parameter schema only (not handler function)
196
+ const canonical = JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters });
197
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
198
+ }
199
+
200
+ export function verifyIntegrity() {
201
+ const mismatches = [];
202
+ for (const tool of HERMES_TOOLS) {
203
+ const actual = toolHash(tool);
204
+ const expected = KNOWN_HASHES[tool.name];
205
+ if (expected && actual !== expected) {
206
+ mismatches.push({ tool: tool.name, expected, actual });
207
+ }
208
+ }
209
+ return mismatches;
210
+ }
211
+
212
+ export function printHashes() {
213
+ console.log('// Current tool definition hashes — paste into KNOWN_HASHES:');
214
+ for (const tool of HERMES_TOOLS) {
215
+ console.log(` ${tool.name}: '${toolHash(tool)}',`);
216
+ }
217
+ }
218
+
219
+ // =============================================================================
220
+ // REGISTRATION HELPER
221
+ // =============================================================================
222
+
223
+ /**
224
+ * Register all Ship Safe tools with a Hermes tool registry instance.
225
+ *
226
+ * @param {object} toolRegistry — Hermes ToolRegistry instance with a .register() method
227
+ * @param {object} options
228
+ * @param {boolean} [options.skipVerification=false] — bypass hash verification (not recommended)
229
+ * @param {boolean} [options.quiet=false] — suppress registration log lines
230
+ */
231
+ export async function registerWithHermes(toolRegistry, options = {}) {
232
+ if (!options.skipVerification) { // ship-safe-ignore — this is the integrity-check implementation, not a bypass
233
+ const mismatches = verifyIntegrity();
234
+ if (mismatches.length > 0) {
235
+ const msg = mismatches.map(m => ` ${m.tool}: expected ${m.expected}, got ${m.actual}`).join('\n');
236
+ throw new Error(`Ship Safe tool registry integrity check failed:\n${msg}\n\nThis may indicate a supply-chain attack. Run ship-safe --version to verify your installation.`);
237
+ }
238
+ }
239
+
240
+ for (const tool of HERMES_TOOLS) {
241
+ if (typeof toolRegistry.register === 'function') {
242
+ toolRegistry.register(tool);
243
+ } else if (typeof toolRegistry.registerTool === 'function') {
244
+ toolRegistry.registerTool(tool);
245
+ } else {
246
+ throw new Error('toolRegistry must have a .register() or .registerTool() method');
247
+ }
248
+ if (!options.quiet) {
249
+ console.log(` [ship-safe] Registered tool: ${tool.name}`);
250
+ }
251
+ }
252
+ }
@@ -865,6 +865,7 @@ const SECURITY_SENSITIVE_PATTERNS = new Set([
865
865
  'id_ed25519',
866
866
  '*.sqlite',
867
867
  '*.db',
868
+ '.projects',
868
869
  ]);
869
870
 
870
871
  /**
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Plugin Loader — Custom Agent Plugin System
3
+ * ============================================
4
+ *
5
+ * Allows users to drop custom security agents into `.ship-safe/agents/` and
6
+ * have them automatically loaded and run alongside the built-in agents.
7
+ *
8
+ * HOW IT WORKS:
9
+ * 1. On startup, loadPlugins(rootPath) scans `.ship-safe/agents/*.js`
10
+ * 2. Each file must export a default class that extends BaseAgent
11
+ * 3. Validated plugins are instantiated and returned for registration
12
+ * 4. buildOrchestrator() calls loadPlugins() and registers the results
13
+ *
14
+ * PLUGIN CONTRACT:
15
+ * A valid plugin must:
16
+ * - Export a default class (ES module)
17
+ * - Extend BaseAgent (from ship-safe's agent framework)
18
+ * - Implement `async analyze(context)` returning an array of findings
19
+ * - Set `this.name` and `this.category` in the constructor
20
+ *
21
+ * EXAMPLE PLUGIN:
22
+ *
23
+ * // .ship-safe/agents/my-rule.js
24
+ * import { BaseAgent, createFinding } from 'ship-safe';
25
+ *
26
+ * export default class MyCustomRule extends BaseAgent {
27
+ * constructor() {
28
+ * super();
29
+ * this.name = 'MyCustomRule';
30
+ * this.category = 'custom';
31
+ * }
32
+ *
33
+ * async analyze({ rootPath, files }) {
34
+ * const findings = [];
35
+ * for (const file of files) {
36
+ * const content = fs.readFileSync(file, 'utf-8');
37
+ * if (content.includes('eval(')) { // ship-safe-ignore — JSDoc example, not real eval
38
+ * findings.push(createFinding({
39
+ * rule: 'CUSTOM_EVAL',
40
+ * severity: 'high',
41
+ * title: 'Dangerous eval() usage', // ship-safe-ignore — JSDoc string literal
42
+ * description: 'eval() can execute arbitrary code', // ship-safe-ignore — JSDoc string literal
43
+ * file,
44
+ * remediation: 'Replace eval() with safer alternatives', // ship-safe-ignore — JSDoc string literal
45
+ * }));
46
+ * }
47
+ * }
48
+ * return findings;
49
+ * }
50
+ * }
51
+ *
52
+ * PLUGIN ISOLATION:
53
+ * Plugins run in the same process but each agent gets its own timeout (30s).
54
+ * A crashing or hanging plugin does not affect other agents.
55
+ *
56
+ * SECURITY NOTE:
57
+ * Plugins are arbitrary code executed from the local filesystem. Never install
58
+ * plugins from untrusted sources. ship-safe will warn if plugins are detected.
59
+ */
60
+
61
+ import fs from 'fs';
62
+ import path from 'path';
63
+ import { pathToFileURL } from 'url';
64
+
65
+ const PLUGIN_DIR = '.ship-safe/agents';
66
+
67
+ /**
68
+ * Load custom agent plugins from .ship-safe/agents/*.js
69
+ *
70
+ * @param {string} rootPath — project root directory
71
+ * @param {object} options — { verbose, quiet }
72
+ * @returns {Promise<object[]>} — array of instantiated agent objects
73
+ */
74
+ export async function loadPlugins(rootPath, options = {}) {
75
+ const pluginDir = path.join(rootPath, PLUGIN_DIR);
76
+
77
+ if (!fs.existsSync(pluginDir)) return [];
78
+
79
+ let files;
80
+ try {
81
+ files = fs.readdirSync(pluginDir)
82
+ .filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
83
+ .map(f => path.join(pluginDir, f));
84
+ } catch {
85
+ return [];
86
+ }
87
+
88
+ if (files.length === 0) return [];
89
+
90
+ if (!options.quiet) {
91
+ console.log(` Loading ${files.length} plugin(s) from ${PLUGIN_DIR}...`);
92
+ }
93
+
94
+ const plugins = [];
95
+
96
+ for (const filePath of files) {
97
+ try {
98
+ const fileUrl = pathToFileURL(filePath).href;
99
+ const mod = await import(fileUrl);
100
+ const PluginClass = mod.default;
101
+
102
+ if (typeof PluginClass !== 'function') {
103
+ if (options.verbose) console.warn(` [plugin] ${path.basename(filePath)}: no default export class`);
104
+ continue;
105
+ }
106
+
107
+ // Validate the plugin before instantiation
108
+ const validation = validatePlugin(PluginClass, filePath);
109
+ if (!validation.valid) {
110
+ console.warn(` [plugin] ${path.basename(filePath)} skipped: ${validation.reason}`);
111
+ continue;
112
+ }
113
+
114
+ const instance = new PluginClass();
115
+
116
+ // Ensure required fields are set after construction
117
+ if (!instance.name) {
118
+ instance.name = path.basename(filePath, '.js');
119
+ }
120
+ if (!instance.category) {
121
+ instance.category = 'custom';
122
+ }
123
+
124
+ plugins.push(instance);
125
+
126
+ if (!options.quiet) {
127
+ console.log(` [plugin] Loaded: ${instance.name} (${instance.category})`);
128
+ }
129
+ } catch (err) {
130
+ console.warn(` [plugin] Failed to load ${path.basename(filePath)}: ${err.message}`);
131
+ }
132
+ }
133
+
134
+ return plugins;
135
+ }
136
+
137
+ /**
138
+ * Validate a plugin class before instantiation.
139
+ * Does static checks only — does not instantiate.
140
+ */
141
+ function validatePlugin(PluginClass, filePath) {
142
+ const name = path.basename(filePath);
143
+
144
+ if (typeof PluginClass !== 'function') {
145
+ return { valid: false, reason: 'default export is not a class/function' };
146
+ }
147
+
148
+ // Check prototype has analyze() — the required method
149
+ const proto = PluginClass.prototype;
150
+ if (typeof proto?.analyze !== 'function') {
151
+ return { valid: false, reason: 'class does not implement analyze()' };
152
+ }
153
+
154
+ return { valid: true };
155
+ }
156
+
157
+ /**
158
+ * List available plugins without loading them.
159
+ * Used by `ship-safe doctor` and `ship-safe plugins list`.
160
+ */
161
+ export function listPluginFiles(rootPath) {
162
+ const pluginDir = path.join(rootPath, PLUGIN_DIR);
163
+ if (!fs.existsSync(pluginDir)) return [];
164
+
165
+ try {
166
+ return fs.readdirSync(pluginDir)
167
+ .filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
168
+ .map(f => ({
169
+ name: path.basename(f, '.js'),
170
+ path: path.join(pluginDir, f),
171
+ size: fs.statSync(path.join(pluginDir, f)).size,
172
+ }));
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Scaffold a new plugin file in .ship-safe/agents/
180
+ */
181
+ export function scaffoldPlugin(rootPath, pluginName) {
182
+ const pluginDir = path.join(rootPath, PLUGIN_DIR);
183
+ if (!fs.existsSync(pluginDir)) {
184
+ fs.mkdirSync(pluginDir, { recursive: true });
185
+ }
186
+
187
+ const safeName = pluginName.replace(/[^a-zA-Z0-9_-]/g, '-');
188
+ const className = safeName.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, c => c.toUpperCase());
189
+ const filePath = path.join(pluginDir, `${safeName}.js`);
190
+
191
+ if (fs.existsSync(filePath)) {
192
+ throw new Error(`Plugin already exists: ${filePath}`);
193
+ }
194
+
195
+ const template = `/**
196
+ * Custom Ship Safe Agent: ${className}
197
+ *
198
+ * Drop this file in .ship-safe/agents/ to have it run automatically
199
+ * as part of every \`ship-safe audit\` or \`ship-safe watch --deep\`.
200
+ *
201
+ * The \`analyze(context)\` method receives:
202
+ * context.rootPath — absolute path to the project root
203
+ * context.files — array of absolute file paths to scan
204
+ * context.recon — recon data (frameworks, databases, auth patterns)
205
+ * context.options — CLI options passed to the scan
206
+ *
207
+ * Return an array of findings using \`createFinding()\`.
208
+ */
209
+
210
+ import fs from 'fs';
211
+
212
+ // BaseAgent and createFinding are available from ship-safe internals.
213
+ // If ship-safe is installed globally, use:
214
+ // import { BaseAgent, createFinding } from 'ship-safe';
215
+ // If running from source:
216
+ // import { BaseAgent, createFinding } from '../agents/base-agent.js';
217
+ let BaseAgent, createFinding;
218
+ try {
219
+ ({ BaseAgent, createFinding } = await import('ship-safe'));
220
+ } catch {
221
+ // Running from source — adjust path if needed
222
+ ({ BaseAgent, createFinding } = await import('../agents/base-agent.js'));
223
+ }
224
+
225
+ export default class ${className} extends BaseAgent {
226
+ constructor() {
227
+ super();
228
+ this.name = '${className}';
229
+ this.category = 'custom'; // or: secrets | injection | auth | config | api | llm
230
+ }
231
+
232
+ async analyze({ rootPath, files = [], recon, options }) {
233
+ const findings = [];
234
+
235
+ for (const file of files) {
236
+ // Skip files you don't care about
237
+ if (!/\\.(js|ts|jsx|tsx|py|rb|go|java)$/.test(file)) continue;
238
+
239
+ let content;
240
+ try {
241
+ content = fs.readFileSync(file, 'utf-8');
242
+ } catch {
243
+ continue;
244
+ }
245
+
246
+ const lines = content.split('\\n');
247
+ for (let i = 0; i < lines.length; i++) {
248
+ const line = lines[i];
249
+ if (/ship-safe-ignore/i.test(line)) continue; // respect suppression comments
250
+
251
+ // Example: flag dangerous eval calls // ship-safe-ignore
252
+ if (/\\beval\\s*\\(/.test(line)) { // ship-safe-ignore — template example in plugin scaffold, not real eval
253
+ findings.push(createFinding({
254
+ rule: '${safeName.toUpperCase().replace(/-/g, '_')}',
255
+ severity: 'high', // critical | high | medium | low
256
+ title: 'Example finding from ${className}',
257
+ description: 'Describe the security risk here.',
258
+ file,
259
+ line: i + 1,
260
+ matched: line.trim().slice(0, 100),
261
+ category: this.category,
262
+ remediation: 'Describe the fix here.',
263
+ confidence: 'medium', // high | medium | low
264
+ }));
265
+ }
266
+ }
267
+ }
268
+
269
+ return findings;
270
+ }
271
+ }
272
+ `;
273
+
274
+ fs.writeFileSync(filePath, template, 'utf-8');
275
+ return filePath;
276
+ }