Skip to content

Commit 1088cd0

Browse files
github-actions[bot]carhartlewisMarfuen
authored
[dev] [carhartlewis] lewis/aikido-integration (#1942)
* feat(aikido): add Aikido security integration with vulnerability checks * feat(aikido): enhance OAuth and variable controllers with improved error handling and logging * feat(aikido): improve repository fetching and error handling in checks * fix(aikido): improve error handling for code repository scanning * fix(aikido): improve error handling for code repository scanning * fix(aikido): enhance error handling and update fetch logic for repositories * fix(aikido): update OAuth client credential handling and improve error logging --------- Co-authored-by: Lewis Carhart <lewis@trycomp.ai> Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent bbcdc21 commit 1088cd0

13 files changed

Lines changed: 1077 additions & 73 deletions

File tree

apps/api/src/integration-platform/controllers/oauth.controller.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,8 @@ export class OAuthController {
403403
};
404404

405405
// Add client credentials based on auth method
406+
// Per OAuth 2.0 RFC 6749 Section 2.3.1, when using HTTP Basic auth (header),
407+
// client credentials should NOT be included in the request body
406408
if (config.clientAuthMethod === 'header') {
407409
const creds = Buffer.from(
408410
`${credentials.clientId}:${credentials.clientSecret}`,
@@ -422,8 +424,10 @@ export class OAuthController {
422424
});
423425

424426
if (!response.ok) {
425-
await response.text(); // consume body
426-
this.logger.error(`Token exchange failed: ${response.status}`);
427+
const errorBody = await response.text();
428+
this.logger.error(
429+
`Token exchange failed: ${response.status} - ${errorBody}`,
430+
);
427431
throw new Error(`Token exchange failed: ${response.status}`);
428432
}
429433

apps/api/src/integration-platform/controllers/variables.controller.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,18 @@ export class VariablesController {
248248

249249
fetch: async <T = unknown>(path: string): Promise<T> => {
250250
const url = new URL(path, baseUrl);
251+
251252
const response = await fetch(url.toString(), {
252253
headers: buildHeaders(),
253254
});
254-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
255-
return response.json();
255+
256+
if (!response.ok) {
257+
const errorText = await response.text();
258+
throw new Error(`HTTP ${response.status}: ${errorText}`);
259+
}
260+
261+
const data = await response.json();
262+
return data as T;
256263
},
257264

258265
fetchAllPages: async <T = unknown>(path: string): Promise<T[]> => {
@@ -268,10 +275,30 @@ export class VariablesController {
268275
const response = await fetch(url.toString(), {
269276
headers: buildHeaders(),
270277
});
271-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
272278

273-
const items: T[] = await response.json();
274-
if (!Array.isArray(items) || items.length === 0) break;
279+
if (!response.ok) {
280+
const errorText = await response.text();
281+
throw new Error(`HTTP ${response.status}: ${errorText}`);
282+
}
283+
284+
const data = await response.json();
285+
286+
// Handle both direct array responses and wrapped responses
287+
// e.g., some APIs return { items: [...] } instead of [...]
288+
let items: T[];
289+
if (Array.isArray(data)) {
290+
items = data;
291+
} else if (data && typeof data === 'object') {
292+
// Find the first array property in the response
293+
const arrayValue = Object.values(data).find((v) =>
294+
Array.isArray(v),
295+
) as T[] | undefined;
296+
items = arrayValue ?? [];
297+
} else {
298+
items = [];
299+
}
300+
301+
if (items.length === 0) break;
275302

276303
allItems.push(...items);
277304
if (items.length < perPage) break;

apps/api/src/integration-platform/services/credential-vault.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ export class CredentialVaultService {
304304
};
305305

306306
// Add client credentials based on auth method
307+
// Per OAuth 2.0 RFC 6749 Section 2.3.1, when using HTTP Basic auth (header),
308+
// client credentials should NOT be included in the request body
307309
if (config.clientAuthMethod === 'header') {
308310
const credentials = Buffer.from(
309311
`${config.clientId}:${config.clientSecret}`,
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Code Repository Scanning Check
3+
*
4+
* Verifies that all code repositories are actively being scanned by Aikido.
5+
* Ensures continuous security monitoring of the codebase.
6+
*/
7+
8+
import { TASK_TEMPLATES } from '../../../task-mappings';
9+
import type { IntegrationCheck } from '../../../types';
10+
import type { AikidoCodeRepositoriesResponse, AikidoCodeRepository } from '../types';
11+
import { targetRepositoriesVariable } from '../variables';
12+
13+
const SCAN_STALE_DAYS = 7; // Consider a scan stale after 7 days
14+
15+
/**
16+
* Check if a scan is stale based on unix timestamp or ISO string
17+
*/
18+
const isStale = (lastScannedAt: number | string | undefined): boolean => {
19+
if (!lastScannedAt) return true;
20+
21+
// Handle both unix timestamp (number) and ISO string
22+
const lastScanMs =
23+
typeof lastScannedAt === 'number' ? lastScannedAt * 1000 : new Date(lastScannedAt).getTime();
24+
25+
const diffDays = (Date.now() - lastScanMs) / (1000 * 60 * 60 * 24);
26+
return diffDays > SCAN_STALE_DAYS;
27+
};
28+
29+
export const codeRepositoryScanningCheck: IntegrationCheck = {
30+
id: 'code_repository_scanning',
31+
name: 'Code Repositories Actively Scanned',
32+
description: 'Verify that all code repositories are being actively scanned for vulnerabilities',
33+
taskMapping: TASK_TEMPLATES.secureCode,
34+
defaultSeverity: 'medium',
35+
36+
variables: [targetRepositoriesVariable],
37+
38+
run: async (ctx) => {
39+
const targetRepoIds = ctx.variables.target_repositories as string[] | undefined;
40+
41+
ctx.log('Fetching code repositories from Aikido');
42+
43+
// Aikido API: https://apidocs.aikido.dev/reference/listcoderepos
44+
// Note: The API returns a direct array without pagination support
45+
// Adding page/per_page params causes empty response
46+
const response = await ctx.fetch<AikidoCodeRepositoriesResponse | AikidoCodeRepository[]>(
47+
'repositories/code',
48+
);
49+
50+
// Handle both array response and wrapped response formats
51+
const allRepos = Array.isArray(response) ? response : (response?.repositories ?? []);
52+
53+
ctx.log(`Found ${allRepos.length} code repositories`);
54+
55+
// Filter to target repos if specified
56+
let repos = allRepos;
57+
if (targetRepoIds && targetRepoIds.length > 0) {
58+
repos = allRepos.filter((repo) => targetRepoIds.includes(String(repo.id)));
59+
ctx.log(`Filtered to ${repos.length} target repositories`);
60+
}
61+
62+
if (repos.length === 0) {
63+
// Differentiate between no repos connected vs filter mismatch
64+
if (targetRepoIds && targetRepoIds.length > 0 && allRepos.length > 0) {
65+
// Repositories exist but none match the target_repositories filter
66+
ctx.fail({
67+
title: 'No matching repositories found',
68+
description: `None of the ${targetRepoIds.length} specified target repository IDs match the ${allRepos.length} connected repositories. This may be due to typos, disconnected repositories, or incorrect IDs in the target_repositories configuration.`,
69+
resourceType: 'workspace',
70+
resourceId: 'aikido-repos',
71+
severity: 'high',
72+
remediation: `1. Verify the repository IDs in your target_repositories configuration
73+
2. Go to Aikido > Repositories to find correct repository IDs
74+
3. Check if the target repositories are still connected
75+
4. Update the target_repositories variable with valid IDs
76+
5. Or remove target_repositories to scan all connected repositories`,
77+
evidence: {
78+
target_repository_ids: targetRepoIds,
79+
connected_repository_count: allRepos.length,
80+
connected_repository_ids: allRepos.map((r) => String(r.id)),
81+
checked_at: new Date().toISOString(),
82+
},
83+
});
84+
} else {
85+
// No repositories connected at all
86+
ctx.fail({
87+
title: 'No code repositories connected',
88+
description:
89+
'No code repositories are connected to Aikido. Connect your repositories to enable security scanning.',
90+
resourceType: 'workspace',
91+
resourceId: 'aikido-repos',
92+
severity: 'high',
93+
remediation: `1. Go to Aikido > Repositories
94+
2. Click "Add Repository" or connect your source control provider
95+
3. Select the repositories you want to scan
96+
4. Enable scanning for each repository`,
97+
evidence: {
98+
total_repos: 0,
99+
checked_at: new Date().toISOString(),
100+
},
101+
});
102+
}
103+
return;
104+
}
105+
106+
for (const repo of repos) {
107+
// Use actual API field names: 'active', 'last_scanned_at', 'name'
108+
const stale = isStale(repo.last_scanned_at ?? repo.last_scan_at);
109+
const inactive = !(repo.active ?? repo.is_active);
110+
const failed = repo.scan_status === 'failed';
111+
const repoName = repo.name || repo.full_name || String(repo.id);
112+
113+
if (inactive) {
114+
ctx.fail({
115+
title: `Repository not active: ${repoName}`,
116+
description: `The repository ${repoName} is not activated for scanning in Aikido.`,
117+
resourceType: 'repository',
118+
resourceId: String(repo.id),
119+
severity: 'medium',
120+
remediation: `1. Go to Aikido > Repositories
121+
2. Find ${repoName}
122+
3. Click "Activate" to enable scanning`,
123+
evidence: {
124+
repo_id: repo.id,
125+
name: repoName,
126+
provider: repo.provider,
127+
active: repo.active ?? repo.is_active,
128+
},
129+
});
130+
} else if (failed) {
131+
ctx.fail({
132+
title: `Scan failed: ${repoName}`,
133+
description: `The last scan for ${repoName} failed.`,
134+
resourceType: 'repository',
135+
resourceId: String(repo.id),
136+
severity: 'high',
137+
remediation: `1. Go to Aikido > Repositories > ${repoName}
138+
2. Check scan logs for error details
139+
3. Verify repository access and permissions
140+
4. Retry the scan`,
141+
evidence: {
142+
repo_id: repo.id,
143+
name: repoName,
144+
provider: repo.provider,
145+
scan_status: repo.scan_status,
146+
last_scanned_at: repo.last_scanned_at,
147+
},
148+
});
149+
} else if (stale) {
150+
const lastScanMs = repo.last_scanned_at
151+
? repo.last_scanned_at * 1000
152+
: repo.last_scan_at
153+
? new Date(repo.last_scan_at).getTime()
154+
: null;
155+
const daysSinceScan = lastScanMs
156+
? Math.floor((Date.now() - lastScanMs) / (1000 * 60 * 60 * 24))
157+
: 'never';
158+
159+
ctx.fail({
160+
title: `Stale scan: ${repoName}`,
161+
description: `Repository ${repoName} hasn't been scanned in over ${SCAN_STALE_DAYS} days.`,
162+
resourceType: 'repository',
163+
resourceId: String(repo.id),
164+
severity: 'low',
165+
remediation: `1. Go to Aikido > Repositories > ${repoName}
166+
2. Click "Scan now" to trigger a new scan
167+
3. Verify webhook integration for automatic scanning`,
168+
evidence: {
169+
repo_id: repo.id,
170+
name: repoName,
171+
provider: repo.provider,
172+
last_scanned_at: repo.last_scanned_at,
173+
days_since_scan: daysSinceScan,
174+
},
175+
});
176+
} else {
177+
ctx.pass({
178+
title: `Repository actively scanned: ${repoName}`,
179+
description: `Repository ${repoName} is active and has been scanned recently.`,
180+
resourceType: 'repository',
181+
resourceId: String(repo.id),
182+
evidence: {
183+
repo_id: repo.id,
184+
name: repoName,
185+
provider: repo.provider,
186+
active: repo.active ?? repo.is_active,
187+
last_scanned_at: repo.last_scanned_at,
188+
checked_at: new Date().toISOString(),
189+
},
190+
});
191+
}
192+
}
193+
},
194+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Aikido Integration Checks
3+
*
4+
* Export all checks for use in the manifest.
5+
*/
6+
7+
export { codeRepositoryScanningCheck } from './code-repository-scanning';
8+
export { issueCountThresholdCheck } from './issue-count-threshold';
9+
export { openSecurityIssuesCheck } from './open-security-issues';

0 commit comments

Comments
 (0)