@securityreviewai/securityreview-kit 0.1.36 → 0.1.37

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 CHANGED
@@ -31,7 +31,7 @@ npx @securityreviewai/securityreview-kit init --switch-project
31
31
  | Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md` |
32
32
  | VS Code Copilot | `vscode` | `.vscode/mcp.json` | `.github/copilot-instructions.md`, `.github/skills/threat-modelling/SKILL.md`, `.github/skills/guardrails-profiler/SKILL.md`, `.github/skills/guardrails-selection/SKILL.md`, `.github/agents/ctm_sync.agent.md`, `.github/hooks/srai-session-policy.json` |
33
33
  | Windsurf | `windsurf` | `.windsurf/mcp_config.json` | `.windsurf/rules/srai-security-review.md` |
34
- | Codex | `codex` | `.codex/config.toml` | `AGENTS.md` |
34
+ | Codex | `codex` | `.codex/config.toml` | `AGENTS.md`, `.agents/skills/threat-modelling/SKILL.md`, `.agents/skills/guardrails-profiler/SKILL.md`, `.agents/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
35
35
  | Gemini CLI | `gemini` | `.gemini/settings.json` | `GEMINI.md` |
36
36
  | Antigravity | `antigravity` | `.gemini/settings.json` | `.agents/rules/srai-security-review.md` |
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securityreviewai/securityreview-kit",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Bootstrap security-review-mcp for AI IDEs and CLI tools",
5
5
  "author": "Debarshi Das <debarshi.das@we45.com>",
6
6
  "license": "UNLICENSED",
@@ -2,6 +2,29 @@ import { join } from 'node:path';
2
2
  import { readText, writeText, ensureDir } from '../../utils/fs-helpers.js';
3
3
  import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
4
4
 
5
+ function ensureHooksFeature(existing) {
6
+ const featureLine = 'codex_hooks = true';
7
+ const featuresHeader = '[features]';
8
+
9
+ if (!existing) {
10
+ return `${featuresHeader}\n${featureLine}`;
11
+ }
12
+
13
+ if (existing.includes(featuresHeader)) {
14
+ const featuresSectionPattern = /(\[features\][\s\S]*?)(?=\n\[[^\]]+\]|$)/;
15
+ return existing.replace(featuresSectionPattern, (section) => {
16
+ if (/\bcodex_hooks\s*=/.test(section)) {
17
+ return section.replace(/codex_hooks\s*=\s*(true|false)/, featureLine);
18
+ }
19
+ const separator = section.endsWith('\n') ? '' : '\n';
20
+ return `${section}${separator}${featureLine}\n`;
21
+ });
22
+ }
23
+
24
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
25
+ return `${existing}${separator}${featuresHeader}\n${featureLine}\n`;
26
+ }
27
+
5
28
  /**
6
29
  * Generate Codex MCP config at .codex/config.toml
7
30
  * Codex uses TOML format. We use simple string templating since the structure
@@ -21,23 +44,25 @@ SECURITY_REVIEW_API_URL = "${envVars.apiUrl}"
21
44
  SECURITY_REVIEW_API_TOKEN = "${envVars.apiToken}"
22
45
  `.trim();
23
46
 
47
+ const existingWithHooks = ensureHooksFeature(existing).trim();
48
+
24
49
  // Check if we already have this server configured
25
- if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
50
+ if (existingWithHooks.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
26
51
  // Replace the existing block — find from the server header to the next
27
52
  // section header or end of file
28
53
  const regex = new RegExp(
29
54
  `\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[(?!mcp_servers\\.${MCP_SERVER_NAME})|$)`,
30
55
  );
31
- const updated = existing.replace(regex, serverBlock);
56
+ const updated = existingWithHooks.replace(regex, serverBlock);
32
57
  writeText(filePath, updated);
33
- } else if (existing) {
58
+ } else if (existingWithHooks) {
34
59
  // Append to existing file
35
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
36
- writeText(filePath, existing + separator + serverBlock + '\n');
60
+ const separator = existingWithHooks.endsWith('\n') ? '\n' : '\n\n';
61
+ writeText(filePath, existingWithHooks + separator + serverBlock + '\n');
37
62
  } else {
38
63
  // New file
39
64
  ensureDir(join(cwd, '.codex'));
40
- writeText(filePath, serverBlock + '\n');
65
+ writeText(filePath, existingWithHooks + '\n\n' + serverBlock + '\n');
41
66
  }
42
67
 
43
68
  return filePath;
@@ -0,0 +1,42 @@
1
+ import { mkdtempSync, mkdirSync, 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 './codex.js';
7
+
8
+ test('Codex MCP generator enables hooks and writes MCP server block', () => {
9
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-mcp-'));
10
+
11
+ generate(cwd, {
12
+ apiUrl: 'https://example.test',
13
+ apiToken: 'secret-token',
14
+ });
15
+
16
+ const config = readFileSync(join(cwd, '.codex/config.toml'), 'utf8');
17
+ assert.match(config, /\[features\]\ncodex_hooks = true/);
18
+ assert.match(config, /\[mcp_servers\.security-review-mcp\]/);
19
+ assert.match(config, /SECURITY_REVIEW_API_URL = "https:\/\/example\.test"/);
20
+ });
21
+
22
+ test('Codex MCP generator preserves existing features while forcing codex_hooks', () => {
23
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-mcp-merge-'));
24
+ const configPath = join(cwd, '.codex', 'config.toml');
25
+ mkdirSync(join(cwd, '.codex'), { recursive: true });
26
+
27
+ writeFileSync(
28
+ configPath,
29
+ '[features]\nshell_snapshot = true\ncodex_hooks = false\n\n[profiles.default]\nmodel = "gpt-5.4"\n',
30
+ 'utf8',
31
+ );
32
+
33
+ generate(cwd, {
34
+ apiUrl: 'https://example.test',
35
+ apiToken: 'secret-token',
36
+ });
37
+
38
+ const config = readFileSync(configPath, 'utf8');
39
+ assert.match(config, /\[features\][\s\S]*shell_snapshot = true/);
40
+ assert.match(config, /\[features\][\s\S]*codex_hooks = true/);
41
+ assert.match(config, /\[profiles\.default\]\nmodel = "gpt-5\.4"/);
42
+ });
@@ -1,28 +1,128 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { join } from 'node:path';
2
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR, GUARDRAILS_SELECTION_SKILL_REL_DIR } from '../../utils/constants.js';
3
+ import {
4
+ CTM_SYNC_AGENT_REL_PATH,
5
+ GUARDRAILS_PROFILER_SKILL_REL_DIR,
6
+ GUARDRAILS_SELECTION_SKILL_REL_DIR,
7
+ THREAT_MODELLING_SKILL_REL_DIR,
8
+ } from '../../utils/constants.js';
3
9
  import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
4
- import { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
10
+ import {
11
+ getCtmSyncWorkflowContent,
12
+ getGuardrailsInitProfileContent,
13
+ getRuleContent,
14
+ getThreatModellingSkillContent,
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 getCodexSessionHookContent() {
24
+ const additionalContext = [
25
+ '## MANDATORY SECURITY GATE (Session Policy)',
26
+ '',
27
+ '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:',
28
+ '',
29
+ '1. Fetch Vibe Guardrails first using .agents/skills/guardrails-selection/SKILL.md. Resolve the project, call get_guardrails, shortlist relevant guardrails, then call get_guardrail_by_id for the shortlist.',
30
+ '2. Run PWNISMS threat modelling using .agents/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
31
+ '3. Implement secure code using both the hydrated guardrails and PWNISMS findings.',
32
+ '4. Invoke the ctm_sync subagent using .codex/agents/ctm_sync.toml after threat modelling or guardrail enforcement. Reuse the exact guardrail shortlist; do not re-query guardrails during CTM sync.',
33
+ '',
34
+ 'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, ctm_sync last.',
35
+ ].join('\n');
36
+
37
+ const commandPayload = JSON.stringify({
38
+ hookSpecificOutput: {
39
+ hookEventName: 'SessionStart',
40
+ additionalContext,
41
+ },
42
+ });
43
+
44
+ return JSON.stringify(
45
+ {
46
+ hooks: {
47
+ SessionStart: [
48
+ {
49
+ matcher: 'startup|resume',
50
+ hooks: [
51
+ {
52
+ type: 'command',
53
+ command: `printf '%s\\n' '${commandPayload.replaceAll("'", "'\\''")}'`,
54
+ timeout: 5,
55
+ statusMessage: 'Loading SRAI security session policy',
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ },
61
+ },
62
+ null,
63
+ 2,
64
+ ) + '\n';
65
+ }
66
+
67
+ function getAgentBody(content) {
68
+ return content.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
69
+ }
70
+
71
+ function getCodexCtmSyncAgentContent(options = {}) {
72
+ const developerInstructions = getAgentBody(getCtmSyncWorkflowContent(options));
73
+ return [
74
+ 'name = "ctm_sync"',
75
+ 'description = "Synchronize threat-model and guardrail data to SRAI after security-relevant work."',
76
+ "developer_instructions = '''",
77
+ developerInstructions,
78
+ "'''",
79
+ '',
80
+ ].join('\n');
81
+ }
5
82
 
6
83
  /**
7
84
  * Generate Codex workspace rule — appends to AGENTS.md
8
85
  */
9
86
  export function generate(cwd, options = {}) {
10
- const filePath = join(cwd, 'AGENTS.md');
11
- const content = getRuleContent({
87
+ const optionsWithSkillDirs = {
12
88
  ...options,
13
89
  guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.codex,
14
- });
90
+ threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.codex,
91
+ ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.codex,
92
+ };
93
+ const filePath = join(cwd, 'AGENTS.md');
94
+ const content = getRuleContent(optionsWithSkillDirs);
15
95
  const action = upsertSentinelBlock(filePath, content);
16
96
 
97
+ const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.codex, 'SKILL.md');
98
+ const threatSkill = writeGeneratedText(
99
+ threatSkillPath,
100
+ getThreatModellingSkillContent(optionsWithSkillDirs),
101
+ );
102
+
103
+ const ctmSyncAgentPath = join(cwd, CTM_SYNC_AGENT_REL_PATH.codex);
104
+ const ctmSyncAgent = writeGeneratedText(
105
+ ctmSyncAgentPath,
106
+ getCodexCtmSyncAgentContent(optionsWithSkillDirs),
107
+ );
108
+
109
+ const hooksPath = join(cwd, '.codex', 'hooks.json');
110
+ const hooks = writeGeneratedText(hooksPath, getCodexSessionHookContent());
111
+
17
112
  const guardrailsInitPath = join(cwd, '.codex', 'commands', 'guardrails-init-profile.md');
18
- const guardrailsInitContent = getGuardrailsInitProfileContent({
19
- ...options,
20
- guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.codex,
21
- });
22
- writeText(guardrailsInitPath, guardrailsInitContent);
113
+ const guardrailsInit = writeGeneratedText(
114
+ guardrailsInitPath,
115
+ getGuardrailsInitProfileContent({
116
+ ...optionsWithSkillDirs,
117
+ guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.codex,
118
+ }),
119
+ );
23
120
 
24
121
  return [
25
122
  { filePath, action, kind: 'rule' },
26
- { filePath: guardrailsInitPath, action: 'created', kind: 'command' },
123
+ { ...threatSkill, kind: 'skill' },
124
+ { ...ctmSyncAgent, kind: 'agent' },
125
+ { ...hooks, kind: 'hooks' },
126
+ { ...guardrailsInit, kind: 'command' },
27
127
  ];
28
128
  }
@@ -0,0 +1,47 @@
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, subagent, 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
+ 'AGENTS.md',
15
+ '.agents/skills/threat-modelling/SKILL.md',
16
+ '.codex/agents/ctm_sync.toml',
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', 'agent', 'hooks', 'command']);
26
+
27
+ const instructions = readFileSync(join(cwd, 'AGENTS.md'), 'utf8');
28
+ assert.match(instructions, /\.agents\/skills\/guardrails-selection\/SKILL\.md/);
29
+ assert.match(instructions, /\.agents\/skills\/threat-modelling\/SKILL\.md/);
30
+ assert.match(instructions, /\.codex\/agents\/ctm_sync\.toml/);
31
+ assert.doesNotMatch(instructions, /\.cursor/);
32
+
33
+ const threatSkill = readFileSync(join(cwd, '.agents/skills/threat-modelling/SKILL.md'), 'utf8');
34
+ for (const heading of ['Product', 'Workload', 'Network', 'IAM', 'Secrets', 'Monitoring', 'Supply Chain']) {
35
+ assert.match(threatSkill, new RegExp(heading));
36
+ }
37
+ assert.match(threatSkill, /\.codex\/agents\/ctm_sync\.toml/);
38
+ assert.doesNotMatch(threatSkill, /get_project_profile_description/);
39
+
40
+ const agent = readFileSync(join(cwd, '.codex/agents/ctm_sync.toml'), 'utf8');
41
+ assert.match(agent, /name = "ctm_sync"/);
42
+ assert.match(agent, /developer_instructions = '''/);
43
+ assert.match(agent, /Configured SRAI project name: `SmokeProject`/);
44
+
45
+ const hooks = JSON.parse(readFileSync(join(cwd, '.codex/hooks.json'), 'utf8'));
46
+ assert.equal(hooks.hooks.SessionStart[0].hooks[0].type, 'command');
47
+ });
@@ -11,7 +11,7 @@ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
11
11
 
12
12
  ## Canonical paths
13
13
 
14
- - **This skill & signal registry (read-only):** `<GUARDRAILS_SKILL_DIR>/` — e.g. `.cursor/skills/guardrails-profiler`, `.github/skills/guardrails-profiler`, `.claude/skills/guardrails-profiler`, or `.codex/skills/guardrails-profiler` depending on where this file was installed.
14
+ - **This skill & signal registry (read-only):** `<GUARDRAILS_SKILL_DIR>/` — e.g. `.cursor/skills/guardrails-profiler`, `.github/skills/guardrails-profiler`, `.claude/skills/guardrails-profiler`, or `.agents/skills/guardrails-profiler` for Codex depending on where this file was installed.
15
15
  - **Signal registry file:** `<GUARDRAILS_SKILL_DIR>/references/signal-registry.json`
16
16
  - **Local guardrails file:** `.guardrails/profile.json`
17
17
  - **Combined manifest (project root):** `profile.json` — includes guardrails profile, vibe profile fields for MCP, and default pack payload
@@ -64,7 +64,7 @@ export const GUARDRAILS_PROFILER_SKILL_REL_DIR = {
64
64
  cursor: '.cursor/skills/guardrails-profiler',
65
65
  claude: '.claude/skills/guardrails-profiler',
66
66
  vscode: '.github/skills/guardrails-profiler',
67
- codex: '.codex/skills/guardrails-profiler',
67
+ codex: '.agents/skills/guardrails-profiler',
68
68
  };
69
69
 
70
70
  /** Relative workspace dirs for the guardrails-selection skill (per IDE / CLI). */
@@ -72,19 +72,21 @@ export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
72
72
  cursor: '.cursor/skills/guardrails-selection',
73
73
  claude: '.claude/skills/guardrails-selection',
74
74
  vscode: '.github/skills/guardrails-selection',
75
- codex: '.codex/skills/guardrails-selection',
75
+ codex: '.agents/skills/guardrails-selection',
76
76
  };
77
77
 
78
78
  /** Relative workspace dirs for the PWNISMS threat-modelling skill (per IDE / CLI). */
79
79
  export const THREAT_MODELLING_SKILL_REL_DIR = {
80
80
  cursor: '.cursor/skills/threat-modelling',
81
81
  vscode: '.github/skills/threat-modelling',
82
+ codex: '.agents/skills/threat-modelling',
82
83
  };
83
84
 
84
85
  /** Relative workspace paths for the CTM sync agent/workflow (per IDE / CLI). */
85
86
  export const CTM_SYNC_AGENT_REL_PATH = {
86
87
  cursor: '.cursor/agents/ctm_sync.md',
87
88
  vscode: '.github/agents/ctm_sync.agent.md',
89
+ codex: '.codex/agents/ctm_sync.toml',
88
90
  };
89
91
 
90
92
  export const SENTINEL_START = '<!-- securityreview-kit:start -->';