@securityreviewai/securityreview-kit 0.1.11
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 +90 -0
- package/bin/securityreview-kit.js +5 -0
- package/package.json +44 -0
- package/src/cli.js +54 -0
- package/src/commands/init.js +332 -0
- package/src/commands/status.js +86 -0
- package/src/commands/switch-project.js +205 -0
- package/src/generators/mcp/claude.js +27 -0
- package/src/generators/mcp/codex.js +44 -0
- package/src/generators/mcp/cursor.js +27 -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 +13 -0
- package/src/generators/rules/codex.js +13 -0
- package/src/generators/rules/content.js +72 -0
- package/src/generators/rules/content.md +93 -0
- package/src/generators/rules/create-ide-workflow.md +34 -0
- package/src/generators/rules/ctm_sync.md +33 -0
- package/src/generators/rules/ctm_sync_rule.md +78 -0
- package/src/generators/rules/cursor.js +84 -0
- package/src/generators/rules/gemini.js +13 -0
- package/src/generators/rules/hooks.json +11 -0
- package/src/generators/rules/skill.md +161 -0
- package/src/generators/rules/srai-profile.md +32 -0
- package/src/generators/rules/vscode.js +13 -0
- package/src/generators/rules/windsurf.js +13 -0
- package/src/utils/constants.js +63 -0
- package/src/utils/detect.js +27 -0
- package/src/utils/fs-helpers.js +82 -0
- package/src/utils/srai.js +183 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { MCP_SERVER_NAME, TARGET_NAMES, TARGETS } from './constants.js';
|
|
4
|
+
import { readJson, readText } from './fs-helpers.js';
|
|
5
|
+
|
|
6
|
+
const PROJECT_LIST_PATH = '/api/projects/';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize API URL from raw input.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeApiUrl(value) {
|
|
12
|
+
const trimmed = String(value || '').trim();
|
|
13
|
+
if (!trimmed) return '';
|
|
14
|
+
|
|
15
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `https://${trimmed}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseTomlEnvValue(content, key) {
|
|
23
|
+
const match = content.match(new RegExp(`${key}\\s*=\\s*"(.*?)"`));
|
|
24
|
+
return match ? match[1].trim() : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractCredentialsFromJsonConfig(content) {
|
|
28
|
+
const servers = content?.mcpServers || content?.servers;
|
|
29
|
+
if (!servers || typeof servers !== 'object') return null;
|
|
30
|
+
|
|
31
|
+
const server = servers[MCP_SERVER_NAME];
|
|
32
|
+
if (!server || typeof server !== 'object') return null;
|
|
33
|
+
|
|
34
|
+
const apiUrl = normalizeApiUrl(server?.env?.SECURITY_REVIEW_API_URL || '');
|
|
35
|
+
const apiToken = String(server?.env?.SECURITY_REVIEW_API_TOKEN || '').trim();
|
|
36
|
+
if (!apiUrl || !apiToken) return null;
|
|
37
|
+
|
|
38
|
+
return { apiUrl, apiToken };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read previously saved API credentials from any configured MCP file.
|
|
43
|
+
*/
|
|
44
|
+
export function getStoredCredentials(cwd) {
|
|
45
|
+
for (const targetName of TARGET_NAMES) {
|
|
46
|
+
const target = TARGETS[targetName];
|
|
47
|
+
const mcpPath = join(cwd, target.mcpConfigPath);
|
|
48
|
+
if (!existsSync(mcpPath)) continue;
|
|
49
|
+
|
|
50
|
+
if (mcpPath.endsWith('.toml')) {
|
|
51
|
+
const content = readText(mcpPath);
|
|
52
|
+
if (!content) continue;
|
|
53
|
+
|
|
54
|
+
const apiUrl = normalizeApiUrl(parseTomlEnvValue(content, 'SECURITY_REVIEW_API_URL'));
|
|
55
|
+
const apiToken = parseTomlEnvValue(content, 'SECURITY_REVIEW_API_TOKEN');
|
|
56
|
+
if (apiUrl && apiToken) {
|
|
57
|
+
return { apiUrl, apiToken };
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const json = readJson(mcpPath);
|
|
63
|
+
const extracted = extractCredentialsFromJsonConfig(json);
|
|
64
|
+
if (extracted) {
|
|
65
|
+
return extracted;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { apiUrl: '', apiToken: '' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveProjectsEndpoint(apiUrl) {
|
|
73
|
+
const parsed = new URL(apiUrl);
|
|
74
|
+
const pathname = parsed.pathname.replace(/\/+$/, '');
|
|
75
|
+
|
|
76
|
+
if (pathname.endsWith('/api/projects')) {
|
|
77
|
+
parsed.pathname = `${pathname}/`;
|
|
78
|
+
} else if (pathname.endsWith('/api')) {
|
|
79
|
+
parsed.pathname = `${pathname}/projects/`;
|
|
80
|
+
} else if (!pathname || pathname === '/') {
|
|
81
|
+
parsed.pathname = PROJECT_LIST_PATH;
|
|
82
|
+
} else {
|
|
83
|
+
parsed.pathname = `${pathname}${PROJECT_LIST_PATH}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
parsed.search = '';
|
|
87
|
+
parsed.hash = '';
|
|
88
|
+
return parsed.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toProjectName(entry) {
|
|
92
|
+
if (typeof entry === 'string') return entry.trim();
|
|
93
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
94
|
+
|
|
95
|
+
const nameFields = [
|
|
96
|
+
'name',
|
|
97
|
+
'projectName',
|
|
98
|
+
'project_name',
|
|
99
|
+
'displayName',
|
|
100
|
+
'display_name',
|
|
101
|
+
'title',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const key of nameFields) {
|
|
105
|
+
const value = entry[key];
|
|
106
|
+
if (typeof value === 'string' && value.trim()) {
|
|
107
|
+
return value.trim();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (entry.project && typeof entry.project === 'object') {
|
|
112
|
+
return toProjectName(entry.project);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractProjectNames(payload) {
|
|
119
|
+
const collections = [];
|
|
120
|
+
|
|
121
|
+
if (Array.isArray(payload)) {
|
|
122
|
+
collections.push(payload);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (payload && typeof payload === 'object') {
|
|
126
|
+
const keys = ['projects', 'data', 'results', 'items'];
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
if (Array.isArray(payload[key])) {
|
|
129
|
+
collections.push(payload[key]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const names = new Set();
|
|
135
|
+
for (const list of collections) {
|
|
136
|
+
for (const entry of list) {
|
|
137
|
+
const name = toProjectName(entry);
|
|
138
|
+
if (name) names.add(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [...names].sort((a, b) => a.localeCompare(b));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fetch all project names from SRAI.
|
|
147
|
+
*/
|
|
148
|
+
export async function fetchProjectNames(apiUrl, apiToken) {
|
|
149
|
+
const endpoint = resolveProjectsEndpoint(apiUrl);
|
|
150
|
+
|
|
151
|
+
let response;
|
|
152
|
+
try {
|
|
153
|
+
response = await fetch(endpoint, {
|
|
154
|
+
method: 'GET',
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Bearer ${apiToken}`,
|
|
157
|
+
Accept: 'application/json',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw new Error(`Could not reach SRAI API at ${endpoint}: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Project fetch failed (${response.status} ${response.statusText}) at ${endpoint}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let payload;
|
|
171
|
+
try {
|
|
172
|
+
payload = await response.json();
|
|
173
|
+
} catch {
|
|
174
|
+
throw new Error(`Project fetch returned non-JSON content at ${endpoint}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const names = extractProjectNames(payload);
|
|
178
|
+
if (names.length === 0) {
|
|
179
|
+
throw new Error(`No project names found in API response from ${endpoint}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return names;
|
|
183
|
+
}
|