Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Code Repository Scanning Check
*
* Verifies that all code repositories are actively being scanned by Aikido.
* Ensures continuous security monitoring of the codebase.
*/

import { TASK_TEMPLATES } from '../../../task-mappings';
import type { IntegrationCheck } from '../../../types';
import type { AikidoCodeRepositoriesResponse, AikidoCodeRepository } from '../types';
import { targetRepositoriesVariable } from '../variables';

const SCAN_STALE_DAYS = 7; // Consider a scan stale after 7 days

const isStale = (lastScanAt: string | undefined): boolean => {
if (!lastScanAt) return true;

const lastScan = new Date(lastScanAt);
const now = new Date();
const diffDays = (now.getTime() - lastScan.getTime()) / (1000 * 60 * 60 * 24);

return diffDays > SCAN_STALE_DAYS;
};

export const codeRepositoryScanningCheck: IntegrationCheck = {
id: 'code_repository_scanning',
name: 'Code Repositories Actively Scanned',
description: 'Verify that all code repositories are being actively scanned for vulnerabilities',
taskMapping: TASK_TEMPLATES.secureCode,
defaultSeverity: 'medium',

variables: [targetRepositoriesVariable],

run: async (ctx) => {
const targetRepoIds = ctx.variables.target_repositories as string[] | undefined;

ctx.log('Fetching code repositories from Aikido');

const response = await ctx.fetch<AikidoCodeRepositoriesResponse>('repositories/code', {
params: { per_page: '100' },
});

let repos: AikidoCodeRepository[] = response.repositories || [];
ctx.log(`Found ${repos.length} code repositories`);

// Filter to target repos if specified
if (targetRepoIds && targetRepoIds.length > 0) {
repos = repos.filter((repo) => targetRepoIds.includes(repo.id));
ctx.log(`Filtered to ${repos.length} target repositories`);
}

if (repos.length === 0) {
ctx.fail({
title: 'No code repositories connected',
description:
'No code repositories are connected to Aikido. Connect your repositories to enable security scanning.',
resourceType: 'workspace',
resourceId: 'aikido-repos',
severity: 'high',
remediation: `1. Go to Aikido > Repositories
2. Click "Add Repository" or connect your source control provider
3. Select the repositories you want to scan
4. Enable scanning for each repository`,
evidence: {
total_repos: 0,
checked_at: new Date().toISOString(),
},
});
return;
}

for (const repo of repos) {
const stale = isStale(repo.last_scan_at);
const inactive = !repo.is_active;
const failed = repo.scan_status === 'failed';

if (inactive) {
ctx.fail({
title: `Repository not active: ${repo.full_name}`,
description: `The repository ${repo.full_name} is not activated for scanning in Aikido.`,
resourceType: 'repository',
resourceId: repo.id,
severity: 'medium',
remediation: `1. Go to Aikido > Repositories
2. Find ${repo.full_name}
3. Click "Activate" to enable scanning`,
evidence: {
repo_id: repo.id,
name: repo.full_name,
provider: repo.provider,
is_active: repo.is_active,
created_at: repo.created_at,
},
});
} else if (failed) {
ctx.fail({
title: `Scan failed: ${repo.full_name}`,
description: `The last scan for ${repo.full_name} failed.`,
resourceType: 'repository',
resourceId: repo.id,
severity: 'high',
remediation: `1. Go to Aikido > Repositories > ${repo.full_name}
2. Check scan logs for error details
3. Verify repository access and permissions
4. Retry the scan`,
evidence: {
repo_id: repo.id,
name: repo.full_name,
provider: repo.provider,
scan_status: repo.scan_status,
last_scan_at: repo.last_scan_at,
},
});
} else if (stale) {
ctx.fail({
title: `Stale scan: ${repo.full_name}`,
description: `Repository ${repo.full_name} hasn't been scanned in over ${SCAN_STALE_DAYS} days.`,
resourceType: 'repository',
resourceId: repo.id,
severity: 'low',
remediation: `1. Go to Aikido > Repositories > ${repo.full_name}
2. Click "Scan now" to trigger a new scan
3. Verify webhook integration for automatic scanning`,
evidence: {
repo_id: repo.id,
name: repo.full_name,
provider: repo.provider,
last_scan_at: repo.last_scan_at,
days_since_scan: repo.last_scan_at
? Math.floor(
(Date.now() - new Date(repo.last_scan_at).getTime()) / (1000 * 60 * 60 * 24),
)
: 'never',
},
});
} else {
ctx.pass({
title: `Repository actively scanned: ${repo.full_name}`,
description: `Repository ${repo.full_name} is active and has been scanned recently.`,
resourceType: 'repository',
resourceId: repo.id,
evidence: {
repo_id: repo.id,
name: repo.full_name,
provider: repo.provider,
is_active: repo.is_active,
scan_status: repo.scan_status,
last_scan_at: repo.last_scan_at,
issues_count: repo.issues_count,
checked_at: new Date().toISOString(),
},
});
}
}
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Aikido Integration Checks
*
* Export all checks for use in the manifest.
*/

export { codeRepositoryScanningCheck } from './code-repository-scanning';
export { issueCountThresholdCheck } from './issue-count-threshold';
export { openSecurityIssuesCheck } from './open-security-issues';
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Issue Count Threshold Check
*
* Monitors the total number of open issues and fails if it exceeds
* a configurable threshold. Useful for maintaining security hygiene.
*/

import { TASK_TEMPLATES } from '../../../task-mappings';
import type { IntegrationCheck } from '../../../types';
import type { AikidoIssueCounts, AikidoSeverity, AikidoSeverityCounts } from '../types';
import { issueThresholdVariable, severityThresholdVariable } from '../variables';

/**
* Calculate the count of issues at or above the severity threshold.
* e.g., if threshold is "high", count critical + high issues.
*/
const countAtOrAboveSeverity = (
counts: AikidoSeverityCounts,
threshold: AikidoSeverity,
): number => {
switch (threshold) {
case 'critical':
return counts.critical;
case 'high':
return counts.critical + counts.high;
case 'medium':
return counts.critical + counts.high + counts.medium;
case 'low':
default:
return counts.all;
}
};

export const issueCountThresholdCheck: IntegrationCheck = {
id: 'issue_count_threshold',
name: 'Issue Count Within Threshold',
description: 'Verify that the total number of open security issues is within acceptable limits',
taskMapping: TASK_TEMPLATES.monitoringAlerting,
defaultSeverity: 'medium',

variables: [severityThresholdVariable, issueThresholdVariable],

run: async (ctx) => {
const severityThreshold = (ctx.variables.severity_threshold as AikidoSeverity) || 'high';
const threshold = (ctx.variables.issue_threshold as number) ?? 0;

ctx.log(
`Fetching issue counts from Aikido (severity: ${severityThreshold}, threshold: ${threshold})`,
);

let counts: AikidoIssueCounts;
try {
counts = await ctx.fetch<AikidoIssueCounts>('issues/counts');
} catch (error) {
ctx.warn('Issue counts endpoint not accessible');
ctx.pass({
title: 'Issue count check skipped',
description: 'Could not fetch issue counts from Aikido.',
resourceType: 'workspace',
resourceId: 'issue-counts',
evidence: {
reason: 'API endpoint not accessible',
checked_at: new Date().toISOString(),
},
});
return;
}

// API returns: { issue_groups: { critical, high, medium, low, all }, issues: { ... } }
const issueGroups = counts?.issue_groups;
const issues = counts?.issues;

if (!issueGroups) {
ctx.warn('No issue group counts in response');
ctx.pass({
title: 'Issue count check skipped',
description: 'Could not parse issue counts from Aikido response.',
resourceType: 'workspace',
resourceId: 'issue-counts',
evidence: {
reason: 'Invalid response format',
checked_at: new Date().toISOString(),
},
});
return;
}

// Count only issues at or above the severity threshold
const openCount = countAtOrAboveSeverity(issueGroups, severityThreshold);
ctx.log(`Found ${openCount} issue groups at ${severityThreshold} severity or above`);

// Build severity breakdown showing what's being counted
const severityLabel =
severityThreshold === 'low'
? 'all severities'
: `${severityThreshold} severity or above`;

const severityBreakdown = `Critical: ${issueGroups.critical}, High: ${issueGroups.high}, Medium: ${issueGroups.medium}, Low: ${issueGroups.low}`;

if (openCount <= threshold) {
ctx.pass({
title: `Issue count within threshold: ${openCount}/${threshold}`,
description: `There are ${openCount} issue groups at ${severityLabel}, which is within the configured threshold of ${threshold}.`,
resourceType: 'workspace',
resourceId: 'issue-counts',
evidence: {
counted_issues: openCount,
severity_filter: severityThreshold,
threshold,
total_issue_groups: issueGroups.all,
total_issues: issues?.all,
issue_groups_by_severity: issueGroups,
issues_by_severity: issues,
checked_at: new Date().toISOString(),
},
});
return;
}

// Determine check severity based on how far over threshold
const overageRatio = openCount / (threshold || 1);
const checkSeverity = overageRatio >= 3 ? 'high' : 'medium';

ctx.fail({
title: `Issue count exceeds threshold: ${openCount}/${threshold}`,
description: `There are ${openCount} issue groups at ${severityLabel}, which exceeds the configured threshold of ${threshold}. ${severityBreakdown}`,
resourceType: 'workspace',
resourceId: 'issue-counts',
severity: checkSeverity,
remediation: `1. Log into Aikido Security dashboard
2. Review open issues by priority (${issueGroups.critical} critical, ${issueGroups.high} high)
3. Address or appropriately snooze/ignore issues
4. Consider adjusting the threshold if the current limit is too restrictive`,
evidence: {
counted_issues: openCount,
severity_filter: severityThreshold,
threshold,
overage: openCount - threshold,
total_issue_groups: issueGroups.all,
total_issues: issues?.all,
issue_groups_by_severity: issueGroups,
issues_by_severity: issues,
checked_at: new Date().toISOString(),
},
});
},
};
Loading