|
| 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 | +}; |
0 commit comments