secure-coding-rules 2.0.0 β 2.0.1
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/README.md +71 -60
- package/package.json +5 -5
- package/src/__tests__/adapters.test.js +201 -0
- package/src/__tests__/loader.test.js +68 -0
- package/src/adapters/agents.js +2 -2
- package/src/adapters/claude.js +42 -3
- package/src/adapters/copilot.js +46 -2
- package/src/i18n.js +271 -0
- package/src/index.js +222 -56
- package/src/loader.js +33 -20
- package/src/prompts.js +79 -77
- package/src/templates/frameworks/express-security.md +130 -0
- package/src/templates/frameworks/nextjs-security.md +149 -0
- package/src/templates/frameworks/react-security.md +120 -0
- package/src/templates/typescript/typescript-security.md +113 -0
package/src/index.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* secure-rules - OWASP 2025 Security Rules Generator for AI Coding Assistants
|
|
3
|
-
*
|
|
4
|
-
* Generates security coding rules in the format of your preferred AI tool:
|
|
5
|
-
* - CLAUDE.md (Claude Code)
|
|
6
|
-
* - .cursor/rules/*.mdc (Cursor)
|
|
7
|
-
* - .windsurf/rules/*.md (Windsurf)
|
|
8
|
-
* - .github/copilot-instructions.md (GitHub Copilot)
|
|
9
|
-
* - AGENTS.md (vendor-neutral)
|
|
2
|
+
* secure-coding-rules - OWASP 2025 Security Rules Generator for AI Coding Assistants
|
|
10
3
|
*/
|
|
11
4
|
|
|
12
5
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
6
|
import { readFileSync, existsSync } from 'node:fs';
|
|
14
7
|
import { join, dirname } from 'node:path';
|
|
15
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { initLang, t } from './i18n.js';
|
|
16
10
|
import { promptUser } from './prompts.js';
|
|
17
11
|
import { loadTemplates } from './loader.js';
|
|
18
12
|
|
|
19
|
-
// Adapter imports
|
|
20
13
|
import * as claudeAdapter from './adapters/claude.js';
|
|
21
14
|
import * as cursorAdapter from './adapters/cursor.js';
|
|
22
15
|
import * as windsurfAdapter from './adapters/windsurf.js';
|
|
@@ -33,7 +26,7 @@ function getVersion() {
|
|
|
33
26
|
);
|
|
34
27
|
return pkg.version;
|
|
35
28
|
} catch {
|
|
36
|
-
return '2.
|
|
29
|
+
return '2.1.0';
|
|
37
30
|
}
|
|
38
31
|
}
|
|
39
32
|
|
|
@@ -45,53 +38,88 @@ const adapters = {
|
|
|
45
38
|
agents: agentsAdapter,
|
|
46
39
|
};
|
|
47
40
|
|
|
41
|
+
// Tools that natively use directory-based output
|
|
42
|
+
const DIRECTORY_NATIVE_TOOLS = ['cursor', 'windsurf'];
|
|
43
|
+
|
|
44
|
+
// Tools that support optional directory mode
|
|
45
|
+
const DIRECTORY_OPTIONAL_TOOLS = ['claude', 'copilot'];
|
|
46
|
+
|
|
48
47
|
export async function run() {
|
|
49
48
|
const args = process.argv.slice(2);
|
|
50
49
|
const version = getVersion();
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
initLang(args);
|
|
52
|
+
|
|
53
53
|
if (args.includes('--help') || args.includes('-h')) {
|
|
54
54
|
printHelp(version);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Version flag
|
|
59
58
|
if (args.includes('--version') || args.includes('-v')) {
|
|
60
59
|
console.log(`secure-coding-rules v${version}`);
|
|
61
60
|
return;
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
// --remove flag
|
|
64
|
+
if (args.includes('--remove')) {
|
|
65
|
+
await removeRules();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dryRun = args.includes('--dry-run');
|
|
64
70
|
const config = await promptUser(args);
|
|
65
71
|
|
|
66
|
-
// --check returns null to signal early exit
|
|
67
72
|
if (config === null) return;
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
console.log(`\nLoading security templates...`);
|
|
74
|
+
console.log(`\n${t('loading')}`);
|
|
72
75
|
const templates = await loadTemplates(config.categories);
|
|
73
76
|
|
|
74
77
|
if (templates.size === 0) {
|
|
75
|
-
console.error('
|
|
78
|
+
console.error(t('noTemplates'));
|
|
76
79
|
process.exit(1);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
console.log(
|
|
82
|
+
console.log(t('loaded', templates.size));
|
|
80
83
|
|
|
81
84
|
const cwd = process.cwd();
|
|
82
85
|
const options = { framework: config.framework, version };
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
// Process each selected tool
|
|
88
|
+
for (const toolName of config.tools) {
|
|
89
|
+
const adapter = adapters[toolName];
|
|
90
|
+
if (!adapter) continue;
|
|
91
|
+
|
|
92
|
+
if (config.tools.length > 1) {
|
|
93
|
+
console.log(`\n${t('generatingFor', adapter.name)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (dryRun) {
|
|
97
|
+
dryRunPreview(adapter, toolName, config, templates, options, cwd);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const useDirectory = shouldUseDirectory(toolName, config.outputMode);
|
|
102
|
+
|
|
103
|
+
if (useDirectory) {
|
|
104
|
+
await generateDirectoryMode(adapter, toolName, templates, options, config, cwd);
|
|
105
|
+
} else if (DIRECTORY_NATIVE_TOOLS.includes(toolName)) {
|
|
106
|
+
await generateMultipleFiles(adapter, templates, options, cwd);
|
|
107
|
+
} else {
|
|
108
|
+
await generateSingleFile(adapter, templates, options, cwd);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!dryRun) {
|
|
113
|
+
console.log(`\nβ
${t('success')}`);
|
|
114
|
+
console.log(`π ${t('reference')}`);
|
|
115
|
+
console.log(`\n${t('runAgain')}\n`);
|
|
90
116
|
}
|
|
117
|
+
}
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
function shouldUseDirectory(toolName, outputMode) {
|
|
120
|
+
if (DIRECTORY_NATIVE_TOOLS.includes(toolName)) return false; // cursor/windsurf already directory-based
|
|
121
|
+
if (DIRECTORY_OPTIONAL_TOOLS.includes(toolName) && outputMode === 'directory') return true;
|
|
122
|
+
return false;
|
|
95
123
|
}
|
|
96
124
|
|
|
97
125
|
async function generateSingleFile(adapter, templates, options, cwd) {
|
|
@@ -108,10 +136,10 @@ async function generateSingleFile(adapter, templates, options, cwd) {
|
|
|
108
136
|
const existing = await readFile(outputPath, 'utf-8');
|
|
109
137
|
const merged = adapter.merge(existing, newContent);
|
|
110
138
|
await writeFile(outputPath, merged, 'utf-8');
|
|
111
|
-
console.log(
|
|
139
|
+
console.log(`π ${t('updated', adapter.outputPath)}`);
|
|
112
140
|
} else {
|
|
113
141
|
await writeFile(outputPath, newContent, 'utf-8');
|
|
114
|
-
console.log(
|
|
142
|
+
console.log(`π ${t('created', adapter.outputPath)}`);
|
|
115
143
|
}
|
|
116
144
|
}
|
|
117
145
|
|
|
@@ -131,47 +159,185 @@ async function generateMultipleFiles(adapter, templates, options, cwd) {
|
|
|
131
159
|
count++;
|
|
132
160
|
}
|
|
133
161
|
|
|
134
|
-
console.log(
|
|
162
|
+
console.log(`π ${t('generated', count, adapter.outputDir)}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function generateDirectoryMode(adapter, toolName, templates, options, config, cwd) {
|
|
166
|
+
// 1. Generate individual rule files in the rules directory
|
|
167
|
+
const rulesDir = join(cwd, adapter.rulesDir);
|
|
168
|
+
|
|
169
|
+
if (!existsSync(rulesDir)) {
|
|
170
|
+
await mkdir(rulesDir, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const files = adapter.formatMultiple(templates, options);
|
|
174
|
+
let count = 0;
|
|
175
|
+
|
|
176
|
+
for (const [filename, content] of files) {
|
|
177
|
+
const filePath = join(rulesDir, filename);
|
|
178
|
+
await writeFile(filePath, content, 'utf-8');
|
|
179
|
+
count++;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`π ${t('generated', count, adapter.rulesDir)}`);
|
|
183
|
+
|
|
184
|
+
// 2. Add/update reference in main file
|
|
185
|
+
const mainPath = join(cwd, adapter.outputPath);
|
|
186
|
+
const mainDir = dirname(mainPath);
|
|
187
|
+
|
|
188
|
+
if (!existsSync(mainDir)) {
|
|
189
|
+
await mkdir(mainDir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const refContent = adapter.formatReference(config.categories, options);
|
|
193
|
+
|
|
194
|
+
if (existsSync(mainPath) && adapter.merge) {
|
|
195
|
+
const existing = await readFile(mainPath, 'utf-8');
|
|
196
|
+
const merged = adapter.merge(existing, refContent);
|
|
197
|
+
await writeFile(mainPath, merged, 'utf-8');
|
|
198
|
+
console.log(`π ${t('refUpdated', adapter.outputPath)}`);
|
|
199
|
+
} else {
|
|
200
|
+
await writeFile(mainPath, refContent, 'utf-8');
|
|
201
|
+
console.log(`π ${t('refCreated', adapter.outputPath)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function dryRunPreview(adapter, toolName, config, templates, options, cwd) {
|
|
206
|
+
console.log(`\nββ ${t('dryRunTitle')} (${adapter.name}) ββββββββββββββββββ`);
|
|
207
|
+
console.log(`${t('dryRunFramework')} ${config.framework}`);
|
|
208
|
+
console.log(`${t('dryRunCategories')} ${config.categories.length}`);
|
|
209
|
+
|
|
210
|
+
const useDirectory = shouldUseDirectory(toolName, config.outputMode);
|
|
211
|
+
|
|
212
|
+
if (useDirectory) {
|
|
213
|
+
const files = adapter.formatMultiple(templates, options);
|
|
214
|
+
console.log(`\n${t('dryRunWouldGenerate', files.size, adapter.rulesDir)}`);
|
|
215
|
+
for (const [filename] of files) {
|
|
216
|
+
console.log(` - ${filename}`);
|
|
217
|
+
}
|
|
218
|
+
console.log(`\n${t('dryRunWouldUpdate', adapter.outputPath)} (reference only)`);
|
|
219
|
+
} else if (DIRECTORY_NATIVE_TOOLS.includes(toolName)) {
|
|
220
|
+
const files = adapter.formatMultiple(templates, options);
|
|
221
|
+
console.log(`\n${t('dryRunWouldGenerate', files.size, adapter.outputDir)}`);
|
|
222
|
+
for (const [filename] of files) {
|
|
223
|
+
console.log(` - ${filename}`);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
const outputPath = join(cwd, adapter.outputPath);
|
|
227
|
+
const content = adapter.format(templates, options);
|
|
228
|
+
const exists = existsSync(outputPath);
|
|
229
|
+
console.log(
|
|
230
|
+
`\n${exists ? t('dryRunWouldUpdate', adapter.outputPath) : t('dryRunWouldCreate', adapter.outputPath)}`
|
|
231
|
+
);
|
|
232
|
+
console.log(t('dryRunSize', (content.length / 1024).toFixed(1)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(`ββ ${t('dryRunApply')} βββββββββββββββββββ\n`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function removeRules() {
|
|
239
|
+
const cwd = process.cwd();
|
|
240
|
+
const SECTION_START = '<!-- js-secure-coding:start -->';
|
|
241
|
+
const SECTION_END = '<!-- js-secure-coding:end -->';
|
|
242
|
+
let removed = 0;
|
|
243
|
+
|
|
244
|
+
// 1. Clean markers from single-file tools
|
|
245
|
+
const singleFiles = ['CLAUDE.md', '.github/copilot-instructions.md', 'AGENTS.md'];
|
|
246
|
+
for (const file of singleFiles) {
|
|
247
|
+
const filePath = join(cwd, file);
|
|
248
|
+
if (!existsSync(filePath)) continue;
|
|
249
|
+
const content = await readFile(filePath, 'utf-8');
|
|
250
|
+
if (!content.includes(SECTION_START)) continue;
|
|
251
|
+
|
|
252
|
+
const before = content.substring(0, content.indexOf(SECTION_START));
|
|
253
|
+
const after = content.substring(
|
|
254
|
+
content.indexOf(SECTION_END) + SECTION_END.length
|
|
255
|
+
);
|
|
256
|
+
const cleaned = (before.trimEnd() + '\n' + after.trimStart()).trim();
|
|
257
|
+
|
|
258
|
+
if (cleaned) {
|
|
259
|
+
await writeFile(filePath, cleaned + '\n', 'utf-8');
|
|
260
|
+
console.log(`ποΈ ${t('removedMarkers', file)}`);
|
|
261
|
+
} else {
|
|
262
|
+
const { unlink } = await import('node:fs/promises');
|
|
263
|
+
await unlink(filePath);
|
|
264
|
+
console.log(`ποΈ ${t('removedFile', file)}`);
|
|
265
|
+
}
|
|
266
|
+
removed++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 2. Remove security-* files from rule directories
|
|
270
|
+
const ruleDirs = [
|
|
271
|
+
{ dir: '.cursor/rules', ext: '.mdc' },
|
|
272
|
+
{ dir: '.windsurf/rules', ext: '.md' },
|
|
273
|
+
{ dir: '.claude/rules', ext: '.md' },
|
|
274
|
+
{ dir: '.github/instructions', ext: '.md' },
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
for (const { dir, ext } of ruleDirs) {
|
|
278
|
+
const dirPath = join(cwd, dir);
|
|
279
|
+
if (!existsSync(dirPath)) continue;
|
|
280
|
+
|
|
281
|
+
const { readdir, unlink } = await import('node:fs/promises');
|
|
282
|
+
const files = await readdir(dirPath);
|
|
283
|
+
const securityFiles = files.filter(
|
|
284
|
+
(f) => f.startsWith('security-') && f.endsWith(ext)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
for (const file of securityFiles) {
|
|
288
|
+
await unlink(join(dirPath, file));
|
|
289
|
+
removed++;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (securityFiles.length > 0) {
|
|
293
|
+
console.log(`ποΈ ${t('removedDir', securityFiles.length, dir)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (removed === 0) {
|
|
298
|
+
console.log(`\n${t('noRulesFound')}`);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`\nβ
${t('removeSuccess')}`);
|
|
301
|
+
}
|
|
135
302
|
}
|
|
136
303
|
|
|
137
304
|
function printHelp(version) {
|
|
138
305
|
console.log(`
|
|
139
306
|
secure-coding-rules v${version}
|
|
140
307
|
|
|
141
|
-
|
|
308
|
+
${t('helpDesc')}
|
|
142
309
|
|
|
143
310
|
Usage:
|
|
144
|
-
npx secure-coding-rules Interactive mode (
|
|
145
|
-
npx secure-coding-rules --yes
|
|
146
|
-
npx secure-coding-rules --check
|
|
311
|
+
npx secure-coding-rules Interactive mode (multi-tool select)
|
|
312
|
+
npx secure-coding-rules --yes Smart defaults
|
|
313
|
+
npx secure-coding-rules --check Project status
|
|
314
|
+
npx secure-coding-rules --dry-run Preview
|
|
315
|
+
npx secure-coding-rules --remove Remove all generated rules
|
|
316
|
+
npx secure-coding-rules --lang ko νκ΅μ΄λ‘ μ€ν
|
|
147
317
|
|
|
148
318
|
Options:
|
|
149
|
-
-y, --yes Non-interactive mode
|
|
150
|
-
--check Show
|
|
319
|
+
-y, --yes Non-interactive mode
|
|
320
|
+
--check Show detected AI tools and frameworks
|
|
321
|
+
--dry-run Preview without writing files
|
|
322
|
+
--remove Remove all security rules (clean uninstall)
|
|
323
|
+
--lang <code> Language: en (default), ko
|
|
151
324
|
-h, --help Show this help
|
|
152
325
|
-v, --version Show version
|
|
153
326
|
|
|
154
|
-
|
|
155
|
-
|
|
327
|
+
Output Modes:
|
|
328
|
+
inline Embed all rules in main file (e.g. CLAUDE.md)
|
|
329
|
+
directory Separate rule files + reference in main file
|
|
330
|
+
(e.g. .claude/rules/security-*.md + CLAUDE.md reference)
|
|
331
|
+
|
|
332
|
+
Supported AI Tools (select multiple):
|
|
333
|
+
- Claude Code β CLAUDE.md or .claude/rules/
|
|
156
334
|
- Cursor β .cursor/rules/*.mdc
|
|
157
335
|
- Windsurf β .windsurf/rules/*.md
|
|
158
|
-
- GitHub Copilot β .github/copilot-instructions.md
|
|
159
|
-
- AGENTS.md β AGENTS.md
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
A03: Supply Chain Failures (NEW in 2025)
|
|
165
|
-
A04: Cryptographic Failures
|
|
166
|
-
A05: Injection
|
|
167
|
-
A06: Insecure Design
|
|
168
|
-
A07: Authentication Failures
|
|
169
|
-
A08: Data Integrity Failures
|
|
170
|
-
A09: Logging & Alerting Failures
|
|
171
|
-
A10: Error Handling (NEW in 2025)
|
|
172
|
-
|
|
173
|
-
+ Frontend: XSS, CSRF, CSP, Secure State Management
|
|
174
|
-
|
|
175
|
-
Homepage: https://github.com/user/secure-coding-rules
|
|
336
|
+
- GitHub Copilot β .github/copilot-instructions.md or .github/instructions/
|
|
337
|
+
- AGENTS.md β AGENTS.md
|
|
338
|
+
|
|
339
|
+
OWASP Top 10 2025: A01-A10 + Frontend (XSS, CSRF, CSP, State)
|
|
340
|
+
|
|
341
|
+
Homepage: https://github.com/kwakseongjae/js-secure-coding-prompt-templates
|
|
176
342
|
`);
|
|
177
343
|
}
|
package/src/loader.js
CHANGED
|
@@ -10,31 +10,40 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
const TEMPLATES_DIR = join(__dirname, 'templates');
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
const CATEGORY_DIRS = {
|
|
14
|
+
// OWASP Top 10 2025
|
|
15
|
+
'access-control': 'core',
|
|
16
|
+
'security-config': 'core',
|
|
17
|
+
'supply-chain': 'core',
|
|
18
|
+
'cryptographic': 'core',
|
|
19
|
+
'injection': 'core',
|
|
20
|
+
'secure-design': 'core',
|
|
21
|
+
'authentication': 'core',
|
|
22
|
+
'data-integrity': 'core',
|
|
23
|
+
'logging-alerting': 'core',
|
|
24
|
+
'error-handling': 'core',
|
|
25
|
+
// Frontend
|
|
26
|
+
'xss-prevention': 'frontend',
|
|
27
|
+
'csrf-protection': 'frontend',
|
|
28
|
+
'csp': 'frontend',
|
|
29
|
+
'secure-state': 'frontend',
|
|
30
|
+
// TypeScript
|
|
31
|
+
'typescript-security': 'typescript',
|
|
32
|
+
// Frameworks
|
|
33
|
+
'react-security': 'frameworks',
|
|
34
|
+
'express-security': 'frameworks',
|
|
35
|
+
'nextjs-security': 'frameworks',
|
|
36
|
+
};
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* Load a single template file by category name
|
|
35
40
|
*/
|
|
36
41
|
export async function loadTemplate(category) {
|
|
37
|
-
const subdir =
|
|
42
|
+
const subdir = CATEGORY_DIRS[category];
|
|
43
|
+
if (!subdir) {
|
|
44
|
+
console.warn(`Warning: Template not found: ${category}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
38
47
|
const filePath = join(TEMPLATES_DIR, subdir, `${category}.md`);
|
|
39
48
|
try {
|
|
40
49
|
return await readFile(filePath, 'utf-8');
|
|
@@ -80,6 +89,10 @@ export function getCategoryInfo(category) {
|
|
|
80
89
|
'csrf-protection': { owasp: 'FE-02', title: 'CSRF Protection', group: 'frontend' },
|
|
81
90
|
'csp': { owasp: 'FE-03', title: 'Content Security Policy', group: 'frontend' },
|
|
82
91
|
'secure-state': { owasp: 'FE-04', title: 'Secure State Management', group: 'frontend' },
|
|
92
|
+
'typescript-security': { owasp: 'TS-01', title: 'TypeScript Security', group: 'typescript' },
|
|
93
|
+
'react-security': { owasp: 'FW-01', title: 'React Security', group: 'frameworks' },
|
|
94
|
+
'express-security': { owasp: 'FW-02', title: 'Express Security', group: 'frameworks' },
|
|
95
|
+
'nextjs-security': { owasp: 'FW-03', title: 'Next.js Security', group: 'frameworks' },
|
|
83
96
|
};
|
|
84
97
|
return info[category] || { owasp: '??', title: category, group: 'unknown' };
|
|
85
98
|
}
|