| 1 | <template> |
| 2 | <div class="@container flex flex-col gap-6"> |
| 3 | <!-- Status Legend --> |
| 4 | <n-card size="small" title="Interpreting Results" embedded> |
| 5 | <div class="grid gap-3 @md:grid-cols-2"> |
| 6 | <div class="flex items-center gap-2"> |
| 7 | <n-tag type="success" size="small">PASS</n-tag> |
| 8 | <span class="text-sm">Control meets baseline</span> |
| 9 | </div> |
| 10 | <div class="flex items-center gap-2"> |
| 11 | <n-tag type="error" size="small">FAIL</n-tag> |
| 12 | <span class="text-sm">Remediation recommended</span> |
| 13 | </div> |
| 14 | <div class="flex items-center gap-2"> |
| 15 | <n-tag type="warning" size="small">WARN</n-tag> |
| 16 | <span class="text-sm">Attention required</span> |
| 17 | </div> |
| 18 | <div class="flex items-center gap-2"> |
| 19 | <n-tag type="default" size="small">SKIP</n-tag> |
| 20 | <span class="text-sm">Cannot evaluate</span> |
| 21 | </div> |
| 22 | </div> |
| 23 | <n-divider /> |
| 24 | <div class="flex flex-col gap-2 text-sm"> |
| 25 | <strong>Skip Reasons:</strong> |
| 26 | <div class="flex flex-col gap-1.5"> |
| 27 | <div> |
| 28 | <code>not_authorized</code> |
| 29 | <span class="text-secondary">— Token/user missing permission</span> |
| 30 | </div> |
| 31 | <div> |
| 32 | <code>not_supported</code> |
| 33 | <span class="text-secondary">— Plan/feature not available</span> |
| 34 | </div> |
| 35 | <div> |
| 36 | <code>error</code> |
| 37 | <span class="text-secondary">— Transient/API error; retry or inspect details</span> |
| 38 | </div> |
| 39 | </div> |
| 40 | </div> |
| 41 | </n-card> |
| 42 | |
| 43 | <!-- Controls Coverage --> |
| 44 | <n-collapse> |
| 45 | <n-collapse-item title="Controls Coverage" name="controls"> |
| 46 | <template #header-extra> |
| 47 | <n-tag size="small" type="info">What We Check</n-tag> |
| 48 | </template> |
| 49 | |
| 50 | <div class="flex flex-col gap-3"> |
| 51 | <n-card size="small" title="Organization-Level (Governance)" embedded> |
| 52 | <div class="divide-border flex flex-col divide-y"> |
| 53 | <div |
| 54 | v-for="control in orgControls" |
| 55 | :key="control.id" |
| 56 | class="flex items-start gap-2 py-2 first:pt-0 last:pb-0" |
| 57 | > |
| 58 | <Icon |
| 59 | class="mt-0.5" |
| 60 | :size="18" |
| 61 | :name="control.critical ? 'ion:alert-circle' : 'ion:checkmark-circle'" |
| 62 | :class="control.critical ? 'text-error' : 'text-success'" |
| 63 | /> |
| 64 | |
| 65 | <div class="flex flex-col gap-0"> |
| 66 | <div class="text-sm font-medium">{{ control.name }}</div> |
| 67 | <div class="text-secondary text-xs">{{ control.description }}</div> |
| 68 | </div> |
| 69 | </div> |
| 70 | </div> |
| 71 | </n-card> |
| 72 | |
| 73 | <n-card size="small" title="Repository-Level (Posture)" embedded> |
| 74 | <div class="divide-border flex flex-col divide-y"> |
| 75 | <div |
| 76 | v-for="control in repoControls" |
| 77 | :key="control.id" |
| 78 | class="flex items-start gap-2 py-2 first:pt-0 last:pb-0" |
| 79 | > |
| 80 | <Icon |
| 81 | class="mt-0.5" |
| 82 | :size="18" |
| 83 | :name="control.critical ? 'ion:alert-circle' : 'ion:checkmark-circle'" |
| 84 | :class="control.critical ? 'text-error' : 'text-success'" |
| 85 | /> |
| 86 | |
| 87 | <div class="flex flex-col gap-0"> |
| 88 | <div class="text-sm font-medium">{{ control.name }}</div> |
| 89 | <div class="text-secondary text-xs">{{ control.description }}</div> |
| 90 | </div> |
| 91 | </div> |
| 92 | </div> |
| 93 | </n-card> |
| 94 | </div> |
| 95 | </n-collapse-item> |
| 96 | |
| 97 | <!-- API Permissions --> |
| 98 | <n-collapse-item title="Required API Permissions" name="permissions"> |
| 99 | <template #header-extra> |
| 100 | <n-tag size="small" type="warning">Read-Only</n-tag> |
| 101 | </template> |
| 102 | |
| 103 | <div class="flex flex-col gap-2"> |
| 104 | <p class="*: flex items-center gap-2 text-sm"> |
| 105 | <Icon name="carbon:information" /> |
| 106 | This audit is intentionally |
| 107 | <strong>read-only</strong> |
| 108 | . No write or admin scopes are required. |
| 109 | </p> |
| 110 | |
| 111 | <n-tabs type="segment" animated> |
| 112 | <n-tab-pane name="fine-grained" tab="Fine-Grained PAT (Recommended)"> |
| 113 | <div class="flex flex-col gap-3"> |
| 114 | <p class="text-warning flex items-center gap-2 text-sm"> |
| 115 | <Icon name="carbon:warning" class="text-warning" /> |
| 116 | Create a fine-grained PAT restricted to only the target organization and repos you |
| 117 | intend to audit. |
| 118 | </p> |
| 119 | |
| 120 | <n-card size="small" title="Organization Permissions (READ)" embedded> |
| 121 | <n-list class="bg-transparent!"> |
| 122 | <n-list-item v-for="perm in fineGrainedOrgPerms" :key="perm.name"> |
| 123 | <template #prefix> |
| 124 | <n-tag :type="perm.required ? 'error' : 'default'" size="small"> |
| 125 | {{ perm.required ? "Required" : "Optional" }} |
| 126 | </n-tag> |
| 127 | </template> |
| 128 | <div> |
| 129 | <div class="text-sm font-medium">{{ perm.name }}</div> |
| 130 | <div class="text-secondary text-xs">{{ perm.description }}</div> |
| 131 | </div> |
| 132 | </n-list-item> |
| 133 | </n-list> |
| 134 | </n-card> |
| 135 | |
| 136 | <n-card size="small" title="Repository Permissions (READ)" embedded> |
| 137 | <n-list class="bg-transparent!"> |
| 138 | <n-list-item v-for="perm in fineGrainedRepoPerms" :key="perm.name"> |
| 139 | <template #prefix> |
| 140 | <n-tag :type="perm.required ? 'error' : 'default'" size="small"> |
| 141 | {{ perm.required ? "Required" : "Optional" }} |
| 142 | </n-tag> |
| 143 | </template> |
| 144 | <div> |
| 145 | <div class="text-sm font-medium">{{ perm.name }}</div> |
| 146 | <div class="text-secondary text-xs">{{ perm.description }}</div> |
| 147 | </div> |
| 148 | </n-list-item> |
| 149 | </n-list> |
| 150 | </n-card> |
| 151 | </div> |
| 152 | </n-tab-pane> |
| 153 | |
| 154 | <n-tab-pane name="classic" tab="Classic PAT (Fallback)"> |
| 155 | <div class="flex flex-col gap-3"> |
| 156 | <p class="text-warning flex items-center gap-2 text-sm"> |
| 157 | <Icon name="carbon:warning" class="text-warning" /> |
| 158 | Classic PATs have broader scope. Use fine-grained PATs when possible. |
| 159 | </p> |
| 160 | |
| 161 | <n-card size="small" title="Required Scopes" embedded> |
| 162 | <n-list class="bg-transparent!"> |
| 163 | <n-list-item v-for="scope in classicScopes" :key="scope.name"> |
| 164 | <template #prefix> |
| 165 | <n-tag :type="scope.required ? 'error' : 'default'" size="small"> |
| 166 | {{ scope.required ? "Required" : "Optional" }} |
| 167 | </n-tag> |
| 168 | </template> |
| 169 | <div> |
| 170 | <div class="text-sm font-medium">{{ scope.name }}</div> |
| 171 | <div class="text-secondary text-xs"> |
| 172 | {{ scope.description }} |
| 173 | </div> |
| 174 | </div> |
| 175 | </n-list-item> |
| 176 | </n-list> |
| 177 | </n-card> |
| 178 | </div> |
| 179 | </n-tab-pane> |
| 180 | </n-tabs> |
| 181 | </div> |
| 182 | </n-collapse-item> |
| 183 | |
| 184 | <!-- API Endpoints --> |
| 185 | <n-collapse-item title="API Endpoints Used" name="endpoints"> |
| 186 | <template #header-extra> |
| 187 | <n-tag size="small">GET Only</n-tag> |
| 188 | </template> |
| 189 | |
| 190 | <div class="flex flex-col gap-3"> |
| 191 | <n-card size="small" title="Organization Endpoints" embedded> |
| 192 | <n-list class="bg-transparent!"> |
| 193 | <n-list-item v-for="endpoint in orgEndpoints" :key="endpoint.path"> |
| 194 | <div class="font-mono text-sm"> |
| 195 | <span class="text-green-500">GET</span> |
| 196 | {{ endpoint.path }} |
| 197 | </div> |
| 198 | <div v-if="endpoint.note" class="text-secondary mt-1 text-xs"> |
| 199 | {{ endpoint.note }} |
| 200 | </div> |
| 201 | </n-list-item> |
| 202 | </n-list> |
| 203 | </n-card> |
| 204 | |
| 205 | <n-card size="small" title="Repository Endpoints" embedded> |
| 206 | <n-list class="bg-transparent!"> |
| 207 | <n-list-item v-for="endpoint in repoEndpoints" :key="endpoint.path"> |
| 208 | <div class="font-mono text-sm"> |
| 209 | <span class="text-green-500">GET</span> |
| 210 | {{ endpoint.path }} |
| 211 | </div> |
| 212 | <div v-if="endpoint.note" class="text-secondary mt-1 text-xs"> |
| 213 | {{ endpoint.note }} |
| 214 | </div> |
| 215 | </n-list-item> |
| 216 | </n-list> |
| 217 | </n-card> |
| 218 | </div> |
| 219 | </n-collapse-item> |
| 220 | </n-collapse> |
| 221 | </div> |
| 222 | </template> |
| 223 | |
| 224 | <script setup lang="ts"> |
| 225 | import { NCard, NCollapse, NCollapseItem, NDivider, NList, NListItem, NTabPane, NTabs, NTag } from "naive-ui" |
| 226 | import Icon from "@/components/common/Icon.vue" |
| 227 | |
| 228 | // Controls data |
| 229 | const orgControls = [ |
| 230 | { |
| 231 | id: "mfa", |
| 232 | name: "MFA Enforcement", |
| 233 | description: "Require two-factor authentication for all organization members", |
| 234 | critical: true |
| 235 | }, |
| 236 | { |
| 237 | id: "org-owners", |
| 238 | name: "Org Owner Minimization", |
| 239 | description: "Limit the number of organization owners to reduce risk", |
| 240 | critical: false |
| 241 | }, |
| 242 | { |
| 243 | id: "outside-collab", |
| 244 | name: "Outside Collaborator Monitoring", |
| 245 | description: "Track external collaborators with access to repositories", |
| 246 | critical: false |
| 247 | }, |
| 248 | { |
| 249 | id: "saml-sso", |
| 250 | name: "SAML SSO Enforced", |
| 251 | description: "Enforce single sign-on for centralized authentication", |
| 252 | critical: true |
| 253 | }, |
| 254 | { |
| 255 | id: "pat-expiry", |
| 256 | name: "PAT Expiration Enforced", |
| 257 | description: "Require personal access tokens to have expiration dates", |
| 258 | critical: false |
| 259 | }, |
| 260 | { |
| 261 | id: "actions-policy", |
| 262 | name: "GitHub Actions Policy", |
| 263 | description: "Control allowed actions, default permissions, and fork PR approvals", |
| 264 | critical: false |
| 265 | }, |
| 266 | { |
| 267 | id: "audit-log", |
| 268 | name: "Audit Log Monitoring", |
| 269 | description: "Monitor control-plane changes via audit log detection pack", |
| 270 | critical: false |
| 271 | } |
| 272 | ] |
| 273 | |
| 274 | const repoControls = [ |
| 275 | { |
| 276 | id: "visibility", |
| 277 | name: "Repository Visibility", |
| 278 | description: "Ensure appropriate public/private visibility settings", |
| 279 | critical: false |
| 280 | }, |
| 281 | { |
| 282 | id: "branch-protection", |
| 283 | name: "Branch Protection", |
| 284 | description: "Enforce approvals, status checks, and prevent force pushes", |
| 285 | critical: true |
| 286 | }, |
| 287 | { |
| 288 | id: "secret-scanning", |
| 289 | name: "Secret Scanning + Push Protection", |
| 290 | description: "Detect and block secrets in code before they're exposed", |
| 291 | critical: true |
| 292 | }, |
| 293 | { |
| 294 | id: "dependabot", |
| 295 | name: "Dependabot Alerts", |
| 296 | description: "Monitor dependencies for known vulnerabilities", |
| 297 | critical: true |
| 298 | }, |
| 299 | { |
| 300 | id: "code-scanning", |
| 301 | name: "Code Scanning", |
| 302 | description: "Identify security vulnerabilities in source code", |
| 303 | critical: false |
| 304 | }, |
| 305 | { |
| 306 | id: "environments", |
| 307 | name: "Deployment Environments", |
| 308 | description: "Protect deployment environments with reviewers and rules", |
| 309 | critical: false |
| 310 | } |
| 311 | ] |
| 312 | |
| 313 | // Fine-grained PAT permissions |
| 314 | const fineGrainedOrgPerms = [ |
| 315 | { |
| 316 | name: "Administration", |
| 317 | description: "Required for org policy, Actions settings, SSO indicator, PAT policy", |
| 318 | required: true |
| 319 | }, |
| 320 | { |
| 321 | name: "Members", |
| 322 | description: "Required for outside collaborator visibility and membership endpoints", |
| 323 | required: true |
| 324 | }, |
| 325 | { |
| 326 | name: "Audit Log", |
| 327 | description: "Required for audit-log detections; otherwise those checks will SKIP", |
| 328 | required: false |
| 329 | } |
| 330 | ] |
| 331 | |
| 332 | const fineGrainedRepoPerms = [ |
| 333 | { |
| 334 | name: "Administration", |
| 335 | description: "Required for branch protection, environments, and repo settings", |
| 336 | required: true |
| 337 | }, |
| 338 | { |
| 339 | name: "Contents", |
| 340 | description: "Often required for workflow-related metadata visibility", |
| 341 | required: true |
| 342 | }, |
| 343 | { |
| 344 | name: "Actions", |
| 345 | description: "Recommended for Actions-related repo metadata", |
| 346 | required: false |
| 347 | }, |
| 348 | { |
| 349 | name: "Security events", |
| 350 | description: "Required for secret scanning, Dependabot alerts, code scanning status", |
| 351 | required: true |
| 352 | } |
| 353 | ] |
| 354 | |
| 355 | // Classic PAT scopes |
| 356 | const classicScopes = [ |
| 357 | { |
| 358 | name: "read:org", |
| 359 | description: "Needed for org membership and org settings", |
| 360 | required: true |
| 361 | }, |
| 362 | { |
| 363 | name: "repo", |
| 364 | description: "Required to read branch protection and repo config on private repos", |
| 365 | required: true |
| 366 | } |
| 367 | ] |
| 368 | |
| 369 | // API endpoints |
| 370 | const orgEndpoints = [ |
| 371 | { path: "/orgs/{org}", note: null }, |
| 372 | { path: "/orgs/{org}/repos", note: null }, |
| 373 | { path: "/orgs/{org}/actions/permissions", note: null }, |
| 374 | { path: "/orgs/{org}/actions/permissions/workflow", note: null }, |
| 375 | { path: "/orgs/{org}/actions/permissions/selected-actions", note: null }, |
| 376 | { path: "/orgs/{org}/personal-access-tokens/policies", note: "May be plan/role gated" }, |
| 377 | { path: "/orgs/{org}/audit-log", note: "Detections; plan/role gated" } |
| 378 | ] |
| 379 | |
| 380 | const repoEndpoints = [ |
| 381 | { path: "/repos/{org}/{repo}", note: "Includes security_and_analysis where available" }, |
| 382 | { path: "/repos/{org}/{repo}/branches/{branch}/protection", note: null }, |
| 383 | { path: "/repos/{org}/{repo}/environments", note: null } |
| 384 | ] |
| 385 | </script> |