diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 16:06:47 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 16:06:47 -0500 |
| commit | d8862a9b78b0d6cf4e11cf537be7678794e08811 (patch) | |
| tree | 4af615370bb122e6706bbefc6deaa044e02b477b /octosentry/SecurityEventStore.swift | |
| download | octosentry-d8862a9b78b0d6cf4e11cf537be7678794e08811.tar.gz octosentry-d8862a9b78b0d6cf4e11cf537be7678794e08811.tar.bz2 octosentry-d8862a9b78b0d6cf4e11cf537be7678794e08811.zip | |
Scaffold octosentry MVP: unified GitHub security alert feed0.1.0
MenuBarExtra popover aggregating Dependabot, code scanning, and secret
scanning alerts for a single repo into one severity-ranked stream. Swift 6
strict concurrency, zero third-party dependencies, PAT-via-env-var auth as
a dev-only shortcut ahead of the device authorization flow.
Diffstat (limited to 'octosentry/SecurityEventStore.swift')
| -rw-r--r-- | octosentry/SecurityEventStore.swift | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/octosentry/SecurityEventStore.swift b/octosentry/SecurityEventStore.swift new file mode 100644 index 0000000..a45d5c7 --- /dev/null +++ b/octosentry/SecurityEventStore.swift @@ -0,0 +1,93 @@ +// +// SecurityEventStore.swift +// octosentry +// +// Holds the fetched event stream for the popover. MVP scope: one +// hardcoded repo, in-memory only, PAT read from GITHUB_TOKEN (spec §13). +// +// Each alert source is fetched independently so a problem with one +// endpoint doesn't blank out the other two. A 403/404 on a single source +// usually just means that alert type is disabled for the repo (or the +// token lacks that one permission) — not a real failure — so those are +// reported as quiet "unavailable" notices rather than alarming errors. +// + +import Foundation +import Observation + +@Observable +final class SecurityEventStore { + private(set) var events: [SecurityEvent] = [] + private(set) var isLoading = false + private(set) var errorMessages: [String] = [] + private(set) var unavailableNotices: [String] = [] + + private let owner = "ccleberg" + private let repo = "cleberg.net" + + func refresh() async { + isLoading = true + errorMessages = [] + unavailableNotices = [] + defer { isLoading = false } + + guard let token = ProcessInfo.processInfo.environment["GITHUB_TOKEN"], !token.isEmpty else { + errorMessages = [GitHubAPIError.missingToken.errorDescription ?? "Missing GITHUB_TOKEN."] + return + } + + let client = GitHubSecurityAPIClient(token: token) + + async let dependabot = fetchSource(label: "Dependabot") { + try await client.fetchDependabotAlerts(owner: self.owner, repo: self.repo) + } + async let codeScanning = fetchSource(label: "Code scanning") { + try await client.fetchCodeScanningAlerts(owner: self.owner, repo: self.repo) + } + async let secretScanning = fetchSource(label: "Secret scanning") { + try await client.fetchSecretScanningAlerts(owner: self.owner, repo: self.repo) + } + + let outcomes = await [dependabot, codeScanning, secretScanning] + + var fetchedEvents: [SecurityEvent] = [] + var errors: [String] = [] + var notices: [String] = [] + for outcome in outcomes { + switch outcome { + case .events(let sourceEvents): + fetchedEvents += sourceEvents + case .unavailable(let label): + notices.append("\(label) alerts aren't available for this repo (disabled, or token lacks that permission).") + case .failed(let label, let message): + errors.append("\(label): \(message)") + } + } + + events = fetchedEvents.sorted { lhs, rhs in + lhs.severity != rhs.severity ? lhs.severity > rhs.severity : lhs.createdAt > rhs.createdAt + } + errorMessages = errors + unavailableNotices = notices + } + + private enum SourceOutcome { + case events([SecurityEvent]) + case unavailable(label: String) + case failed(label: String, message: String) + } + + private func fetchSource( + label: String, + _ operation: () async throws -> [SecurityEvent] + ) async -> SourceOutcome { + do { + return .events(try await operation()) + } catch GitHubAPIError.forbidden, GitHubAPIError.notFound { + return .unavailable(label: label) + } catch { + let description = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + return .failed(label: label, message: description) + } + } +} |
