@securityreviewai/securityreview-kit 0.1.50 → 0.1.52
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 +105 -0
- package/bin/securityreview-kit.js +5 -0
- package/package.json +30 -24
- package/src/cli.js +109 -0
- package/src/commands/init.js +851 -0
- package/src/commands/status.js +99 -0
- package/src/commands/switch-project.js +207 -0
- package/src/generators/mcp/claude.js +85 -0
- package/src/generators/mcp/claude.test.js +64 -0
- package/src/generators/mcp/codex.js +70 -0
- package/src/generators/mcp/codex.test.js +43 -0
- package/src/generators/mcp/cursor.js +29 -0
- package/src/generators/mcp/cursor.test.js +50 -0
- package/src/generators/mcp/gemini.js +28 -0
- package/src/generators/mcp/vscode.js +29 -0
- package/src/generators/mcp/windsurf.js +27 -0
- package/src/generators/rules/antigravity.js +22 -0
- package/src/generators/rules/claude.js +87 -0
- package/src/generators/rules/claude.test.js +60 -0
- package/src/generators/rules/codex.js +141 -0
- package/src/generators/rules/codex.test.js +59 -0
- package/src/generators/rules/content.js +110 -0
- package/src/generators/rules/cursor.js +128 -0
- package/src/generators/rules/gemini.js +13 -0
- package/src/generators/rules/guardrails-init-profile.md +56 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +130 -0
- package/src/generators/rules/guardrails-profiler/references/signal-registry.json +514 -0
- package/src/generators/rules/guardrails-selection/references/category-threat-map.md +232 -0
- package/src/generators/rules/guardrails_rule.md +94 -0
- package/src/generators/rules/hooks.json +11 -0
- package/src/generators/rules/srai-profile.md +32 -0
- package/src/generators/rules/vscode.js +101 -0
- package/src/generators/rules/vscode.test.js +54 -0
- package/src/generators/rules/windsurf.js +13 -0
- package/src/utils/constants.js +95 -0
- package/src/utils/cursor-agent-path.js +67 -0
- package/src/utils/cursor-cli-permissions.js +28 -0
- package/src/utils/detect.js +27 -0
- package/src/utils/fs-helpers.js +82 -0
- package/src/utils/guardrails-profiler-bundle.js +84 -0
- package/src/utils/ide-cli-install.js +138 -0
- package/src/utils/profiler-agent.js +446 -0
- package/src/utils/profiler-agent.test.js +81 -0
- package/src/utils/srai.js +252 -0
- package/dist/api.js +0 -44
- package/dist/commands/guardrails.js +0 -13
- package/dist/commands/init.js +0 -88
- package/dist/commands/profile.js +0 -14
- package/dist/commands/status.js +0 -27
- package/dist/commands/sync.js +0 -6
- package/dist/config.js +0 -18
- package/dist/fs.js +0 -43
- package/dist/index.js +0 -44
- package/dist/profile.js +0 -113
- package/dist/scaffold/claude-code.js +0 -43
- package/dist/scaffold/codex.js +0 -41
- package/dist/scaffold/cursor.js +0 -45
- package/dist/scaffold/gemini.js +0 -10
- package/dist/scaffold/index.js +0 -22
- package/dist/scaffold/mcp.js +0 -15
- package/dist/scaffold/rules.js +0 -191
- package/dist/scaffold/vibreview.js +0 -30
- package/dist/scaffold/vscode.js +0 -28
- package/dist/scaffold/windsurf.js +0 -10
- package/dist/sync/index.js +0 -34
- package/dist/sync/payload.js +0 -23
- package/dist/sync/state.js +0 -12
- package/dist/types.js +0 -1
- package/templates/claude/CLAUDE.md +0 -13
- package/templates/claude/agents/guardrail_profiler.md +0 -12
- package/templates/claude/agents/threat_modeler.md +0 -5
- package/templates/claude/skills/vibreview/SKILL.md +0 -21
- package/templates/claude/skills/vibreview/guardrail_patterns.md +0 -12
- package/templates/cursor/rules/vibreview-security.mdc +0 -8
- /package/{templates/shared → src/generators/rules}/content.md +0 -0
- /package/{templates/shared/guardrails-selection.md → src/generators/rules/guardrails-selection/SKILL.md} +0 -0
- /package/{templates/shared/threat-modelling.md → src/generators/rules/skill.md} +0 -0
- /package/{templates/shared → src/generators/rules}/vibereview-sync/SKILL.md +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate VS Code Copilot MCP config at .vscode/mcp.json
|
|
7
|
+
* Uses the VS Code input variable pattern for secure credential prompting.
|
|
8
|
+
*/
|
|
9
|
+
export function generate(cwd, envVars) {
|
|
10
|
+
const filePath = join(cwd, '.vscode', 'mcp.json');
|
|
11
|
+
const existing = readJson(filePath) || {};
|
|
12
|
+
|
|
13
|
+
if (!existing.servers) {
|
|
14
|
+
existing.servers = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
existing.servers[MCP_SERVER_NAME] = {
|
|
18
|
+
type: 'stdio',
|
|
19
|
+
command: 'npx',
|
|
20
|
+
args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
|
|
21
|
+
env: {
|
|
22
|
+
SECURITY_REVIEW_API_URL: envVars.apiUrl,
|
|
23
|
+
SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
writeJson(filePath, existing);
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Windsurf MCP config at .windsurf/mcp_config.json
|
|
7
|
+
*/
|
|
8
|
+
export function generate(cwd, envVars) {
|
|
9
|
+
const filePath = join(cwd, '.windsurf', 'mcp_config.json');
|
|
10
|
+
const existing = readJson(filePath) || {};
|
|
11
|
+
|
|
12
|
+
if (!existing.mcpServers) {
|
|
13
|
+
existing.mcpServers = {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
existing.mcpServers[MCP_SERVER_NAME] = {
|
|
17
|
+
command: 'npx',
|
|
18
|
+
args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
|
|
19
|
+
env: {
|
|
20
|
+
SECURITY_REVIEW_API_URL: envVars.apiUrl,
|
|
21
|
+
SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
writeJson(filePath, existing);
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { writeText } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { getRuleContent } from './content.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Antigravity workspace rule at .agent/rules/srai-security-review.md
|
|
7
|
+
* Antigravity uses .agent/rules/ with YAML frontmatter (trigger: always_on).
|
|
8
|
+
*/
|
|
9
|
+
export function generate(cwd, options = {}) {
|
|
10
|
+
const filePath = join(cwd, '.agent', 'rules', 'srai-security-review.md');
|
|
11
|
+
const content = getRuleContent(options);
|
|
12
|
+
|
|
13
|
+
const rule = `---
|
|
14
|
+
trigger: always_on
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
${content}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
writeText(filePath, rule);
|
|
21
|
+
return filePath;
|
|
22
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
5
|
+
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
6
|
+
SENTINEL_END,
|
|
7
|
+
SENTINEL_START,
|
|
8
|
+
THREAT_MODELLING_SKILL_REL_DIR,
|
|
9
|
+
VIBEREVIEW_SYNC_SKILL_REL_DIR,
|
|
10
|
+
} from '../../utils/constants.js';
|
|
11
|
+
import { readText, upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
12
|
+
import {
|
|
13
|
+
getGuardrailsInitProfileContent,
|
|
14
|
+
getRuleContent,
|
|
15
|
+
getThreatModellingSkillContent,
|
|
16
|
+
getVibeReviewSyncSkillContent,
|
|
17
|
+
} from './content.js';
|
|
18
|
+
|
|
19
|
+
function writeGeneratedText(filePath, content) {
|
|
20
|
+
const action = existsSync(filePath) ? 'updated' : 'created';
|
|
21
|
+
writeText(filePath, content);
|
|
22
|
+
return { filePath, action };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate Claude Code workspace rule — appends to .claude/CLAUDE.md
|
|
27
|
+
*/
|
|
28
|
+
export function generate(cwd, options = {}) {
|
|
29
|
+
const optionsWithSkillDirs = {
|
|
30
|
+
...options,
|
|
31
|
+
guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.claude,
|
|
32
|
+
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.claude,
|
|
33
|
+
vibereviewSyncSkillDir: VIBEREVIEW_SYNC_SKILL_REL_DIR.claude,
|
|
34
|
+
};
|
|
35
|
+
const legacyRootPath = join(cwd, 'CLAUDE.md');
|
|
36
|
+
const filePath = join(cwd, '.claude', 'CLAUDE.md');
|
|
37
|
+
const content = getRuleContent(optionsWithSkillDirs);
|
|
38
|
+
const action = upsertSentinelBlock(filePath, content);
|
|
39
|
+
|
|
40
|
+
const legacyContent = readText(legacyRootPath);
|
|
41
|
+
if (legacyContent.includes(SENTINEL_START) && legacyContent.includes(SENTINEL_END)) {
|
|
42
|
+
const startIdx = legacyContent.indexOf(SENTINEL_START);
|
|
43
|
+
const endIdx = legacyContent.indexOf(SENTINEL_END);
|
|
44
|
+
const before = legacyContent.substring(0, startIdx);
|
|
45
|
+
const after = legacyContent.substring(endIdx + SENTINEL_END.length);
|
|
46
|
+
const migrated = `${before}${after}`.trim();
|
|
47
|
+
if (migrated) {
|
|
48
|
+
writeText(legacyRootPath, migrated + '\n');
|
|
49
|
+
} else if (existsSync(legacyRootPath)) {
|
|
50
|
+
unlinkSync(legacyRootPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.claude, 'SKILL.md');
|
|
55
|
+
const threatSkill = writeGeneratedText(
|
|
56
|
+
threatSkillPath,
|
|
57
|
+
getThreatModellingSkillContent(optionsWithSkillDirs),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const vibereviewSyncSkillPath = join(cwd, VIBEREVIEW_SYNC_SKILL_REL_DIR.claude, 'SKILL.md');
|
|
61
|
+
const vibereviewSyncSkill = writeGeneratedText(
|
|
62
|
+
vibereviewSyncSkillPath,
|
|
63
|
+
getVibeReviewSyncSkillContent(optionsWithSkillDirs),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const deletedLegacyAgentPath = join(cwd, '.claude', 'agents', 'ctm_sync.md');
|
|
67
|
+
const deletedLegacyAgent = existsSync(deletedLegacyAgentPath)
|
|
68
|
+
? (unlinkSync(deletedLegacyAgentPath), { filePath: deletedLegacyAgentPath, action: 'deleted' })
|
|
69
|
+
: null;
|
|
70
|
+
|
|
71
|
+
const guardrailsInitPath = join(cwd, '.claude', 'commands', 'guardrails-init-profile.md');
|
|
72
|
+
const guardrailsInit = writeGeneratedText(
|
|
73
|
+
guardrailsInitPath,
|
|
74
|
+
getGuardrailsInitProfileContent({
|
|
75
|
+
...optionsWithSkillDirs,
|
|
76
|
+
guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.claude,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return [
|
|
81
|
+
{ filePath, action, kind: 'rule' },
|
|
82
|
+
{ ...threatSkill, kind: 'skill' },
|
|
83
|
+
{ ...vibereviewSyncSkill, kind: 'skill' },
|
|
84
|
+
{ ...guardrailsInit, kind: 'command' },
|
|
85
|
+
...(deletedLegacyAgent ? [{ ...deletedLegacyAgent, kind: 'cleanup' }] : []),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { generate } from './claude.js';
|
|
7
|
+
|
|
8
|
+
test('Claude generator writes .claude/CLAUDE.md, threat skill, and profiling command', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-'));
|
|
10
|
+
|
|
11
|
+
const results = generate(cwd, { projectName: 'SmokeProject' });
|
|
12
|
+
|
|
13
|
+
const expectedPaths = [
|
|
14
|
+
'.claude/CLAUDE.md',
|
|
15
|
+
'.claude/skills/threat-modelling/SKILL.md',
|
|
16
|
+
'.claude/skills/vibereview-sync/SKILL.md',
|
|
17
|
+
'.claude/commands/guardrails-init-profile.md',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const relPath of expectedPaths) {
|
|
21
|
+
assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'skill', 'command']);
|
|
25
|
+
|
|
26
|
+
const instructions = readFileSync(join(cwd, '.claude/CLAUDE.md'), 'utf8');
|
|
27
|
+
assert.match(instructions, /\.claude\/skills\/guardrails-selection\/SKILL\.md/);
|
|
28
|
+
assert.match(instructions, /\.claude\/skills\/threat-modelling\/SKILL\.md/);
|
|
29
|
+
assert.match(instructions, /\.claude\/skills\/vibereview-sync\/SKILL\.md/);
|
|
30
|
+
assert.match(instructions, /sync_ai_ide_markdown/);
|
|
31
|
+
assert.match(instructions, /vibereview\//);
|
|
32
|
+
|
|
33
|
+
const threatSkill = readFileSync(join(cwd, '.claude/skills/threat-modelling/SKILL.md'), 'utf8');
|
|
34
|
+
assert.match(threatSkill, /sync_ai_ide_markdown/);
|
|
35
|
+
assert.match(threatSkill, /vibereview\//);
|
|
36
|
+
assert.doesNotMatch(threatSkill, /get_project_profile_description/);
|
|
37
|
+
|
|
38
|
+
const vibereviewSkill = readFileSync(join(cwd, '.claude/skills/vibereview-sync/SKILL.md'), 'utf8');
|
|
39
|
+
assert.match(vibereviewSkill, /Do not read other/i);
|
|
40
|
+
assert.match(vibereviewSkill, /sync_ai_ide_markdown/);
|
|
41
|
+
assert.match(vibereviewSkill, /Event Identity/);
|
|
42
|
+
assert.match(vibereviewSkill, /Practice 1/);
|
|
43
|
+
assert.match(vibereviewSkill, /practice_name:/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Claude generator migrates the kit-managed root CLAUDE.md block into .claude/CLAUDE.md', () => {
|
|
47
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-migrate-'));
|
|
48
|
+
const legacyPath = join(cwd, 'CLAUDE.md');
|
|
49
|
+
|
|
50
|
+
writeFileSync(
|
|
51
|
+
legacyPath,
|
|
52
|
+
'<!-- securityreview-kit:start -->\nold managed block\n<!-- securityreview-kit:end -->\n',
|
|
53
|
+
'utf8',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
generate(cwd, { projectName: 'SmokeProject' });
|
|
57
|
+
|
|
58
|
+
assert.equal(existsSync(join(cwd, '.claude/CLAUDE.md')), true);
|
|
59
|
+
assert.equal(existsSync(legacyPath), false);
|
|
60
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
5
|
+
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
6
|
+
THREAT_MODELLING_SKILL_REL_DIR,
|
|
7
|
+
VIBEREVIEW_SYNC_SKILL_REL_DIR,
|
|
8
|
+
} from '../../utils/constants.js';
|
|
9
|
+
import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
10
|
+
import {
|
|
11
|
+
getGuardrailsInitProfileContent,
|
|
12
|
+
getRuleContent,
|
|
13
|
+
getThreatModellingSkillContent,
|
|
14
|
+
getVibeReviewSyncSkillContent,
|
|
15
|
+
} from './content.js';
|
|
16
|
+
|
|
17
|
+
function writeGeneratedText(filePath, content) {
|
|
18
|
+
const action = existsSync(filePath) ? 'updated' : 'created';
|
|
19
|
+
writeText(filePath, content);
|
|
20
|
+
return { filePath, action };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function removeGeneratedText(filePath) {
|
|
24
|
+
if (existsSync(filePath)) {
|
|
25
|
+
unlinkSync(filePath);
|
|
26
|
+
return { filePath, action: 'deleted' };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getCodexSessionHookContent() {
|
|
32
|
+
const additionalContext = [
|
|
33
|
+
'## MANDATORY SECURITY GATE (Session Policy)',
|
|
34
|
+
'',
|
|
35
|
+
'For any request with security impact (auth, authorization, crypto, input handling, secrets, network, data storage, dependencies, new APIs/endpoints, infrastructure, or code handling untrusted data), follow this order:',
|
|
36
|
+
'',
|
|
37
|
+
'1. Fetch Vibe Guardrails first using .codex/skills/guardrails-selection/SKILL.md. Resolve the project, call get_guardrails, shortlist relevant guardrails, then call get_guardrail_by_id for the shortlist.',
|
|
38
|
+
'2. Run PWNISMS threat modelling using .codex/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
|
|
39
|
+
'3. Implement secure code using both the hydrated guardrails and PWNISMS findings.',
|
|
40
|
+
'4. Read .codex/skills/vibereview-sync/SKILL.md. Write a structured .md artifact under vibereview/, do not read sibling markdown files there just to infer format, validate it, and call sync_ai_ide_markdown directly after threat modelling or guardrail enforcement. Reuse the exact guardrail shortlist; do not re-query guardrails during the final sync.',
|
|
41
|
+
'',
|
|
42
|
+
'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, VibeReview sync last.',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
const sessionStartPayload = JSON.stringify({
|
|
46
|
+
hookSpecificOutput: {
|
|
47
|
+
hookEventName: 'SessionStart',
|
|
48
|
+
additionalContext,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const userPromptPayload = JSON.stringify({
|
|
52
|
+
hookSpecificOutput: {
|
|
53
|
+
hookEventName: 'UserPromptSubmit',
|
|
54
|
+
additionalContext,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return JSON.stringify(
|
|
59
|
+
{
|
|
60
|
+
hooks: {
|
|
61
|
+
SessionStart: [
|
|
62
|
+
{
|
|
63
|
+
matcher: 'startup|resume',
|
|
64
|
+
hooks: [
|
|
65
|
+
{
|
|
66
|
+
type: 'command',
|
|
67
|
+
command: `printf '%s\\n' '${sessionStartPayload.replaceAll("'", "'\\''")}'`,
|
|
68
|
+
timeout: 5,
|
|
69
|
+
statusMessage: 'Loading SRAI security session policy',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
UserPromptSubmit: [
|
|
75
|
+
{
|
|
76
|
+
hooks: [
|
|
77
|
+
{
|
|
78
|
+
type: 'command',
|
|
79
|
+
command: `printf '%s\\n' '${userPromptPayload.replaceAll("'", "'\\''")}'`,
|
|
80
|
+
timeout: 5,
|
|
81
|
+
statusMessage: 'Refreshing SRAI security session policy',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
null,
|
|
89
|
+
2,
|
|
90
|
+
) + '\n';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate Codex workspace rule — appends to AGENTS.md
|
|
95
|
+
*/
|
|
96
|
+
export function generate(cwd, options = {}) {
|
|
97
|
+
const optionsWithSkillDirs = {
|
|
98
|
+
...options,
|
|
99
|
+
guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.codex,
|
|
100
|
+
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.codex,
|
|
101
|
+
vibereviewSyncSkillDir: VIBEREVIEW_SYNC_SKILL_REL_DIR.codex,
|
|
102
|
+
};
|
|
103
|
+
const filePath = join(cwd, '.codex', 'AGENTS.md');
|
|
104
|
+
const content = getRuleContent(optionsWithSkillDirs);
|
|
105
|
+
const action = upsertSentinelBlock(filePath, content);
|
|
106
|
+
|
|
107
|
+
const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.codex, 'SKILL.md');
|
|
108
|
+
const threatSkill = writeGeneratedText(
|
|
109
|
+
threatSkillPath,
|
|
110
|
+
getThreatModellingSkillContent(optionsWithSkillDirs),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const vibereviewSyncSkillPath = join(cwd, VIBEREVIEW_SYNC_SKILL_REL_DIR.codex, 'SKILL.md');
|
|
114
|
+
const vibereviewSyncSkill = writeGeneratedText(
|
|
115
|
+
vibereviewSyncSkillPath,
|
|
116
|
+
getVibeReviewSyncSkillContent(optionsWithSkillDirs),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const deletedLegacyAgent = removeGeneratedText(join(cwd, '.codex', 'agents', 'ctm_sync.toml'));
|
|
120
|
+
|
|
121
|
+
const hooksPath = join(cwd, '.codex', 'hooks.json');
|
|
122
|
+
const hooks = writeGeneratedText(hooksPath, getCodexSessionHookContent());
|
|
123
|
+
|
|
124
|
+
const guardrailsInitPath = join(cwd, '.codex', 'commands', 'guardrails-init-profile.md');
|
|
125
|
+
const guardrailsInit = writeGeneratedText(
|
|
126
|
+
guardrailsInitPath,
|
|
127
|
+
getGuardrailsInitProfileContent({
|
|
128
|
+
...optionsWithSkillDirs,
|
|
129
|
+
guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.codex,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
{ filePath, action, kind: 'rule' },
|
|
135
|
+
{ ...threatSkill, kind: 'skill' },
|
|
136
|
+
{ ...vibereviewSyncSkill, kind: 'skill' },
|
|
137
|
+
{ ...hooks, kind: 'hooks' },
|
|
138
|
+
{ ...guardrailsInit, kind: 'command' },
|
|
139
|
+
...(deletedLegacyAgent ? [{ ...deletedLegacyAgent, kind: 'cleanup' }] : []),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { generate } from './codex.js';
|
|
7
|
+
|
|
8
|
+
test('Codex generator writes AGENTS, skills, hooks, and profiling command', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-'));
|
|
10
|
+
|
|
11
|
+
const results = generate(cwd, { projectName: 'SmokeProject' });
|
|
12
|
+
|
|
13
|
+
const expectedPaths = [
|
|
14
|
+
'.codex/AGENTS.md',
|
|
15
|
+
'.codex/skills/threat-modelling/SKILL.md',
|
|
16
|
+
'.codex/skills/vibereview-sync/SKILL.md',
|
|
17
|
+
'.codex/hooks.json',
|
|
18
|
+
'.codex/commands/guardrails-init-profile.md',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const relPath of expectedPaths) {
|
|
22
|
+
assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'skill', 'hooks', 'command']);
|
|
26
|
+
|
|
27
|
+
const instructions = readFileSync(join(cwd, '.codex/AGENTS.md'), 'utf8');
|
|
28
|
+
assert.match(instructions, /\.codex\/skills\/guardrails-selection\/SKILL\.md/);
|
|
29
|
+
assert.match(instructions, /\.codex\/skills\/threat-modelling\/SKILL\.md/);
|
|
30
|
+
assert.match(instructions, /\.codex\/skills\/vibereview-sync\/SKILL\.md/);
|
|
31
|
+
assert.match(instructions, /sync_ai_ide_markdown/);
|
|
32
|
+
assert.match(instructions, /vibereview\//);
|
|
33
|
+
assert.doesNotMatch(instructions, /\.cursor/);
|
|
34
|
+
assert.doesNotMatch(instructions, /\.agents\/skills/);
|
|
35
|
+
|
|
36
|
+
const threatSkill = readFileSync(join(cwd, '.codex/skills/threat-modelling/SKILL.md'), 'utf8');
|
|
37
|
+
for (const heading of ['Product', 'Workload', 'Network', 'IAM', 'Secrets', 'Monitoring', 'Supply Chain']) {
|
|
38
|
+
assert.match(threatSkill, new RegExp(heading));
|
|
39
|
+
}
|
|
40
|
+
assert.match(threatSkill, /sync_ai_ide_markdown/);
|
|
41
|
+
assert.match(threatSkill, /vibereview\//);
|
|
42
|
+
assert.doesNotMatch(threatSkill, /get_project_profile_description/);
|
|
43
|
+
|
|
44
|
+
const vibereviewSkill = readFileSync(join(cwd, '.codex/skills/vibereview-sync/SKILL.md'), 'utf8');
|
|
45
|
+
assert.match(vibereviewSkill, /do not read other `?\.md`? files in `?vibereview\/`?/i);
|
|
46
|
+
assert.match(vibereviewSkill, /sync_ai_ide_markdown/);
|
|
47
|
+
assert.match(vibereviewSkill, /Event Identity/);
|
|
48
|
+
assert.match(vibereviewSkill, /OWASP Top 10 2025 Mappings/);
|
|
49
|
+
assert.match(vibereviewSkill, /Practice 1/);
|
|
50
|
+
assert.match(vibereviewSkill, /practice_name:/);
|
|
51
|
+
|
|
52
|
+
const hooks = JSON.parse(readFileSync(join(cwd, '.codex/hooks.json'), 'utf8'));
|
|
53
|
+
assert.equal(hooks.hooks.SessionStart[0].hooks[0].type, 'command');
|
|
54
|
+
assert.equal(hooks.hooks.UserPromptSubmit[0].hooks[0].type, 'command');
|
|
55
|
+
assert.match(hooks.hooks.UserPromptSubmit[0].hooks[0].command, /"hookEventName":"UserPromptSubmit"/);
|
|
56
|
+
assert.match(hooks.hooks.UserPromptSubmit[0].hooks[0].command, /vibereview/);
|
|
57
|
+
assert.match(hooks.hooks.UserPromptSubmit[0].hooks[0].command, /sync_ai_ide_markdown/);
|
|
58
|
+
assert.match(hooks.hooks.UserPromptSubmit[0].hooks[0].command, /vibereview-sync\/SKILL\.md/);
|
|
59
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function sanitizeProjectName(value) {
|
|
8
|
+
return value.replace(/[\r\n`]/g, ' ').trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function injectPathPlaceholder(content, placeholder, fallbackPath, configuredPath) {
|
|
12
|
+
if (!content.includes(placeholder)) {
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const resolvedPath =
|
|
17
|
+
typeof configuredPath === 'string' && configuredPath.trim() ? configuredPath.trim() : fallbackPath;
|
|
18
|
+
|
|
19
|
+
return content.replaceAll(placeholder, resolvedPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reads a markdown template and injects the configured project name placeholder.
|
|
24
|
+
*/
|
|
25
|
+
function readTemplate(templateFileName, options = {}) {
|
|
26
|
+
const template = readFileSync(join(__dirname, templateFileName), 'utf-8').trim();
|
|
27
|
+
const rawProjectName = typeof options.projectName === 'string' ? options.projectName : '';
|
|
28
|
+
const projectName = sanitizeProjectName(rawProjectName);
|
|
29
|
+
const resolvedProjectName = projectName || '<SRAI_PROJECT_NAME>';
|
|
30
|
+
|
|
31
|
+
let out = template
|
|
32
|
+
.replaceAll('{{SRAI_PROJECT_NAME}}', resolvedProjectName)
|
|
33
|
+
.replaceAll('<SRAI_PROJECT_NAME>', resolvedProjectName);
|
|
34
|
+
|
|
35
|
+
out = injectPathPlaceholder(
|
|
36
|
+
out,
|
|
37
|
+
'{{GUARDRAILS_SKILL_DIR}}',
|
|
38
|
+
'.cursor/skills/guardrails-profiler',
|
|
39
|
+
options.guardrailsSkillDir,
|
|
40
|
+
);
|
|
41
|
+
out = injectPathPlaceholder(
|
|
42
|
+
out,
|
|
43
|
+
'{{GUARDRAILS_SELECTION_SKILL_DIR}}',
|
|
44
|
+
'.cursor/skills/guardrails-selection',
|
|
45
|
+
options.guardrailsSelectionSkillDir,
|
|
46
|
+
);
|
|
47
|
+
out = injectPathPlaceholder(
|
|
48
|
+
out,
|
|
49
|
+
'{{THREAT_MODELLING_SKILL_DIR}}',
|
|
50
|
+
'.cursor/skills/threat-modelling',
|
|
51
|
+
options.threatModellingSkillDir,
|
|
52
|
+
);
|
|
53
|
+
out = injectPathPlaceholder(
|
|
54
|
+
out,
|
|
55
|
+
'{{VIBEREVIEW_SYNC_SKILL_DIR}}',
|
|
56
|
+
'.cursor/skills/vibereview-sync',
|
|
57
|
+
options.vibereviewSyncSkillDir,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the shared rule content markdown.
|
|
65
|
+
*/
|
|
66
|
+
export function getRuleContent(options = {}) {
|
|
67
|
+
return readTemplate('content.md', options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the Cursor profile uploader command markdown.
|
|
72
|
+
*/
|
|
73
|
+
export function getProfileCommandContent(options = {}) {
|
|
74
|
+
return readTemplate('srai-profile.md', options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the threat-modelling skill markdown.
|
|
79
|
+
*/
|
|
80
|
+
export function getThreatModellingSkillContent(options = {}) {
|
|
81
|
+
return readTemplate('skill.md', options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the VibeReview sync skill markdown.
|
|
86
|
+
*/
|
|
87
|
+
export function getVibeReviewSyncSkillContent(options = {}) {
|
|
88
|
+
return readTemplate(join('vibereview-sync', 'SKILL.md'), options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the vibe guardrails always-applied rule markdown.
|
|
93
|
+
*/
|
|
94
|
+
export function getGuardrailsRuleContent(options = {}) {
|
|
95
|
+
return readTemplate('guardrails_rule.md', options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Guardrails init profile command (repo scan + .guardrails/profile.json + SRAI upload).
|
|
100
|
+
*/
|
|
101
|
+
export function getGuardrailsInitProfileContent(options = {}) {
|
|
102
|
+
return readTemplate('guardrails-init-profile.md', options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the hooks.json content for Cursor session hooks.
|
|
107
|
+
*/
|
|
108
|
+
export function getHooksContent() {
|
|
109
|
+
return readFileSync(join(__dirname, 'hooks.json'), 'utf-8').trim();
|
|
110
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
5
|
+
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
6
|
+
THREAT_MODELLING_SKILL_REL_DIR,
|
|
7
|
+
VIBEREVIEW_SYNC_SKILL_REL_DIR,
|
|
8
|
+
} from '../../utils/constants.js';
|
|
9
|
+
import { writeText } from '../../utils/fs-helpers.js';
|
|
10
|
+
import { mergeCursorCliMcpAllowlist } from '../../utils/cursor-cli-permissions.js';
|
|
11
|
+
import {
|
|
12
|
+
getRuleContent,
|
|
13
|
+
getProfileCommandContent,
|
|
14
|
+
getThreatModellingSkillContent,
|
|
15
|
+
getVibeReviewSyncSkillContent,
|
|
16
|
+
getGuardrailsRuleContent,
|
|
17
|
+
getGuardrailsInitProfileContent,
|
|
18
|
+
getHooksContent,
|
|
19
|
+
} from './content.js';
|
|
20
|
+
|
|
21
|
+
function writeCursorRule(filePath, description, content) {
|
|
22
|
+
const action = existsSync(filePath) ? 'updated' : 'created';
|
|
23
|
+
const mdc = `---
|
|
24
|
+
description: ${description}
|
|
25
|
+
alwaysApply: true
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
${content}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
writeText(filePath, mdc);
|
|
32
|
+
return { filePath, action };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeCursorCommand(filePath, content) {
|
|
36
|
+
const action = existsSync(filePath) ? 'updated' : 'created';
|
|
37
|
+
writeText(filePath, content);
|
|
38
|
+
return { filePath, action };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function removeGeneratedText(filePath) {
|
|
42
|
+
if (existsSync(filePath)) {
|
|
43
|
+
unlinkSync(filePath);
|
|
44
|
+
return { filePath, action: 'deleted' };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate Cursor workspace rules at .cursor/rules/*.mdc
|
|
51
|
+
* Cursor uses .mdc format with YAML front matter.
|
|
52
|
+
*/
|
|
53
|
+
export function generate(cwd, options = {}) {
|
|
54
|
+
const optionsWithSkillDirs = {
|
|
55
|
+
...options,
|
|
56
|
+
guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.cursor,
|
|
57
|
+
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.cursor,
|
|
58
|
+
vibereviewSyncSkillDir: VIBEREVIEW_SYNC_SKILL_REL_DIR.cursor,
|
|
59
|
+
};
|
|
60
|
+
const baseRulePath = join(cwd, '.cursor', 'rules', 'srai-security-review.mdc');
|
|
61
|
+
const guardrailsRulePath = join(cwd, '.cursor', 'rules', 'guardrails_rule.mdc');
|
|
62
|
+
const profileCommandPath = join(cwd, '.cursor', 'commands', 'srai-profile.md');
|
|
63
|
+
const guardrailsInitProfileCommandPath = join(cwd, '.cursor', 'commands', 'guardrails-init-profile.md');
|
|
64
|
+
const skillPath = join(cwd, '.cursor', 'skills', 'threat-modelling', 'SKILL.md');
|
|
65
|
+
const hooksPath = join(cwd, '.cursor', 'hooks.json');
|
|
66
|
+
|
|
67
|
+
const baseRuleContent = getRuleContent(optionsWithSkillDirs);
|
|
68
|
+
const guardrailsRuleContent = getGuardrailsRuleContent(optionsWithSkillDirs);
|
|
69
|
+
const profileCommandContent = getProfileCommandContent(optionsWithSkillDirs);
|
|
70
|
+
const guardrailsInitProfileCommandContent = getGuardrailsInitProfileContent({
|
|
71
|
+
...optionsWithSkillDirs,
|
|
72
|
+
guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.cursor,
|
|
73
|
+
});
|
|
74
|
+
const skillContent = getThreatModellingSkillContent(optionsWithSkillDirs);
|
|
75
|
+
const vibereviewSyncSkillContent = getVibeReviewSyncSkillContent(optionsWithSkillDirs);
|
|
76
|
+
|
|
77
|
+
const baseRule = writeCursorRule(
|
|
78
|
+
baseRulePath,
|
|
79
|
+
'SRAI Security Review gate — consult security-review-mcp before security-relevant code changes',
|
|
80
|
+
baseRuleContent,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const guardrailsRule = writeCursorRule(
|
|
84
|
+
guardrailsRulePath,
|
|
85
|
+
'Vibe Guardrails — fetch and enforce project-specific secure coding guardrails from SRAI',
|
|
86
|
+
guardrailsRuleContent,
|
|
87
|
+
);
|
|
88
|
+
const deletedLegacyRule = removeGeneratedText(join(cwd, '.cursor', 'rules', 'ctm_sync_rule.mdc'));
|
|
89
|
+
const deletedLegacyCommand = removeGeneratedText(join(cwd, '.cursor', 'commands', 'ctm_sync.md'));
|
|
90
|
+
const deletedLegacyAgent = removeGeneratedText(join(cwd, '.cursor', 'agents', 'ctm_sync.md'));
|
|
91
|
+
const deletedLegacyWorkflowCommand = removeGeneratedText(
|
|
92
|
+
join(cwd, '.cursor', 'commands', 'create-ide-workflow.md'),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const profileCommand = writeCursorCommand(profileCommandPath, profileCommandContent);
|
|
96
|
+
const guardrailsInitProfileCommand = writeCursorCommand(
|
|
97
|
+
guardrailsInitProfileCommandPath,
|
|
98
|
+
guardrailsInitProfileCommandContent,
|
|
99
|
+
);
|
|
100
|
+
const skillAction = existsSync(skillPath) ? 'updated' : 'created';
|
|
101
|
+
writeText(skillPath, skillContent);
|
|
102
|
+
const vibereviewSyncSkillPath = join(cwd, VIBEREVIEW_SYNC_SKILL_REL_DIR.cursor, 'SKILL.md');
|
|
103
|
+
const vibereviewSyncSkillAction = existsSync(vibereviewSyncSkillPath) ? 'updated' : 'created';
|
|
104
|
+
writeText(vibereviewSyncSkillPath, vibereviewSyncSkillContent);
|
|
105
|
+
|
|
106
|
+
const hooksContent = getHooksContent();
|
|
107
|
+
const hooksAction = existsSync(hooksPath) ? 'updated' : 'created';
|
|
108
|
+
writeText(hooksPath, hooksContent);
|
|
109
|
+
|
|
110
|
+
const cursorCliPath = join(cwd, '.cursor', 'cli.json');
|
|
111
|
+
const cliPermissionsExisted = existsSync(cursorCliPath);
|
|
112
|
+
mergeCursorCliMcpAllowlist(cwd);
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
{ ...baseRule, kind: 'rule' },
|
|
116
|
+
{ ...guardrailsRule, kind: 'rule' },
|
|
117
|
+
{ ...profileCommand, kind: 'command' },
|
|
118
|
+
{ ...guardrailsInitProfileCommand, kind: 'command' },
|
|
119
|
+
{ filePath: skillPath, action: skillAction, kind: 'skill' },
|
|
120
|
+
{ filePath: vibereviewSyncSkillPath, action: vibereviewSyncSkillAction, kind: 'skill' },
|
|
121
|
+
{ filePath: hooksPath, action: hooksAction, kind: 'hooks' },
|
|
122
|
+
{ filePath: cursorCliPath, action: cliPermissionsExisted ? 'updated' : 'created', kind: 'config' },
|
|
123
|
+
...(deletedLegacyRule ? [{ ...deletedLegacyRule, kind: 'cleanup' }] : []),
|
|
124
|
+
...(deletedLegacyCommand ? [{ ...deletedLegacyCommand, kind: 'cleanup' }] : []),
|
|
125
|
+
...(deletedLegacyAgent ? [{ ...deletedLegacyAgent, kind: 'cleanup' }] : []),
|
|
126
|
+
...(deletedLegacyWorkflowCommand ? [{ ...deletedLegacyWorkflowCommand, kind: 'cleanup' }] : []),
|
|
127
|
+
];
|
|
128
|
+
}
|