CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
advisories.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| f60ccde | 1 | /** |
| 2 | * Block J2 — Security advisories + per-repo alerts. | |
| 3 | * | |
| 4 | * This module does three things: | |
| 5 | * 1. Provides a seeded set of well-known public advisories (log4j, lodash, | |
| 6 | * minimist, etc.) that get inserted into `security_advisories` on first | |
| 7 | * run. | |
| 8 | * 2. Exposes a minimal version-range matcher (`rangeMatches`) that can tell | |
| 9 | * whether a declared manifest spec like `"^1.2.0"` or `">=0.5"` falls | |
| 10 | * inside an advisory's `affected_range` such as `"<1.2.3"`. This is a | |
| 11 | * heuristic — we never claim to replace npm's full semver resolver, but | |
| 12 | * for unpinned or tightly-pinned dev manifests (the common case on | |
| 13 | * GitHub), it's accurate in the common cases. | |
| 14 | * 3. `scanRepositoryForAlerts(repositoryId)` iterates the repo's | |
| 15 | * `repo_dependencies` rows, finds matching advisories, and upserts | |
| 16 | * `repo_advisory_alerts` rows. Existing alerts whose underlying dep | |
| 17 | * went away are auto-closed (status=fixed). | |
| 18 | */ | |
| 19 | ||
| 20 | import { and, desc, eq, inArray } from "drizzle-orm"; | |
| 21 | import { db } from "../db"; | |
| 22 | import { | |
| 23 | repoAdvisoryAlerts, | |
| 24 | repoDependencies, | |
| 25 | securityAdvisories, | |
| 26 | type RepoAdvisoryAlert, | |
| 27 | type SecurityAdvisory, | |
| 28 | } from "../db/schema"; | |
| 29 | ||
| 30 | // ---------------------------------------------------------------------------- | |
| 31 | // Seed: a small but real set of widely-cited advisories | |
| 32 | // ---------------------------------------------------------------------------- | |
| 33 | ||
| 34 | export interface SeedAdvisory { | |
| 35 | ghsaId: string; | |
| 36 | cveId?: string; | |
| 37 | summary: string; | |
| 38 | severity: "low" | "moderate" | "high" | "critical"; | |
| 39 | ecosystem: string; | |
| 40 | packageName: string; | |
| 41 | affectedRange: string; | |
| 42 | fixedVersion?: string; | |
| 43 | referenceUrl?: string; | |
| 44 | } | |
| 45 | ||
| 46 | export const SEED_ADVISORIES: SeedAdvisory[] = [ | |
| 47 | { | |
| 48 | ghsaId: "GHSA-jfh8-c2jp-5v3q", | |
| 49 | cveId: "CVE-2021-44228", | |
| 50 | summary: | |
| 51 | "Log4Shell: Apache Log4j2 JNDI features do not protect against attacker-controlled LDAP and other JNDI related endpoints", | |
| 52 | severity: "critical", | |
| 53 | ecosystem: "composer", | |
| 54 | packageName: "apache/log4j", | |
| 55 | affectedRange: ">=2.0 <2.15.0", | |
| 56 | fixedVersion: "2.15.0", | |
| 57 | referenceUrl: "https://nvd.nist.gov/vuln/detail/CVE-2021-44228", | |
| 58 | }, | |
| 59 | { | |
| 60 | ghsaId: "GHSA-p6mc-m468-83gw", | |
| 61 | cveId: "CVE-2019-10744", | |
| 62 | summary: "Prototype pollution in lodash", | |
| 63 | severity: "high", | |
| 64 | ecosystem: "npm", | |
| 65 | packageName: "lodash", | |
| 66 | affectedRange: "<4.17.12", | |
| 67 | fixedVersion: "4.17.12", | |
| 68 | referenceUrl: "https://github.com/advisories/GHSA-jf85-cpcp-j695", | |
| 69 | }, | |
| 70 | { | |
| 71 | ghsaId: "GHSA-vh95-rmgr-6w4m", | |
| 72 | cveId: "CVE-2020-7598", | |
| 73 | summary: "Prototype pollution in minimist", | |
| 74 | severity: "moderate", | |
| 75 | ecosystem: "npm", | |
| 76 | packageName: "minimist", | |
| 77 | affectedRange: "<1.2.2", | |
| 78 | fixedVersion: "1.2.2", | |
| 79 | referenceUrl: "https://github.com/advisories/GHSA-vh95-rmgr-6w4m", | |
| 80 | }, | |
| 81 | { | |
| 82 | ghsaId: "GHSA-hpx4-r86g-5jrg", | |
| 83 | cveId: "CVE-2020-28469", | |
| 84 | summary: "Regex DoS in glob-parent", | |
| 85 | severity: "high", | |
| 86 | ecosystem: "npm", | |
| 87 | packageName: "glob-parent", | |
| 88 | affectedRange: "<5.1.2", | |
| 89 | fixedVersion: "5.1.2", | |
| 90 | referenceUrl: "https://github.com/advisories/GHSA-ww39-953v-wcq6", | |
| 91 | }, | |
| 92 | { | |
| 93 | ghsaId: "GHSA-rxrx-xcr5-2f33", | |
| 94 | cveId: "CVE-2022-25883", | |
| 95 | summary: "Regex DoS in node-semver", | |
| 96 | severity: "moderate", | |
| 97 | ecosystem: "npm", | |
| 98 | packageName: "semver", | |
| 99 | affectedRange: "<5.7.2", | |
| 100 | fixedVersion: "5.7.2", | |
| 101 | referenceUrl: "https://github.com/advisories/GHSA-c2qf-rxjj-qqgw", | |
| 102 | }, | |
| 103 | { | |
| 104 | ghsaId: "GHSA-j8xg-fqg3-53r7", | |
| 105 | cveId: "CVE-2022-24999", | |
| 106 | summary: "Express qs prototype pollution", | |
| 107 | severity: "high", | |
| 108 | ecosystem: "npm", | |
| 109 | packageName: "qs", | |
| 110 | affectedRange: "<6.9.7", | |
| 111 | fixedVersion: "6.9.7", | |
| 112 | referenceUrl: "https://github.com/advisories/GHSA-hrpp-h998-j3pp", | |
| 113 | }, | |
| 114 | { | |
| 115 | ghsaId: "GHSA-6v2p-pv7g-wrx2", | |
| 116 | cveId: "CVE-2022-36067", | |
| 117 | summary: "vm2 sandbox escape", | |
| 118 | severity: "critical", | |
| 119 | ecosystem: "npm", | |
| 120 | packageName: "vm2", | |
| 121 | affectedRange: "<3.9.11", | |
| 122 | fixedVersion: "3.9.11", | |
| 123 | referenceUrl: "https://github.com/advisories/GHSA-6v2p-pv7g-wrx2", | |
| 124 | }, | |
| 125 | { | |
| 126 | ghsaId: "GHSA-mh63-6h87-95cp", | |
| 127 | cveId: "CVE-2022-21222", | |
| 128 | summary: "Prototype pollution in jquery.extend", | |
| 129 | severity: "moderate", | |
| 130 | ecosystem: "npm", | |
| 131 | packageName: "jquery", | |
| 132 | affectedRange: "<3.5.0", | |
| 133 | fixedVersion: "3.5.0", | |
| 134 | referenceUrl: "https://github.com/advisories/GHSA-gxr4-xjj5-5px2", | |
| 135 | }, | |
| 136 | { | |
| 137 | ghsaId: "GHSA-h52j-qf5x-j8ch", | |
| 138 | cveId: "CVE-2021-33503", | |
| 139 | summary: "urllib3 catastrophic backtracking regex", | |
| 140 | severity: "high", | |
| 141 | ecosystem: "pypi", | |
| 142 | packageName: "urllib3", | |
| 143 | affectedRange: "<1.26.5", | |
| 144 | fixedVersion: "1.26.5", | |
| 145 | referenceUrl: "https://github.com/advisories/GHSA-q2q7-5pp4-w6pg", | |
| 146 | }, | |
| 147 | { | |
| 148 | ghsaId: "GHSA-56pw-mpj4-fxww", | |
| 149 | cveId: "CVE-2019-11236", | |
| 150 | summary: "CRLF injection in urllib3", | |
| 151 | severity: "moderate", | |
| 152 | ecosystem: "pypi", | |
| 153 | packageName: "urllib3", | |
| 154 | affectedRange: "<1.24.2", | |
| 155 | fixedVersion: "1.24.2", | |
| 156 | referenceUrl: "https://github.com/advisories/GHSA-wqvq-5m8c-6g24", | |
| 157 | }, | |
| 158 | { | |
| 159 | ghsaId: "GHSA-w596-4wvx-j9j6", | |
| 160 | cveId: "CVE-2021-33026", | |
| 161 | summary: "Flask-Caching pickle deserialisation RCE", | |
| 162 | severity: "high", | |
| 163 | ecosystem: "pypi", | |
| 164 | packageName: "flask-caching", | |
| 165 | affectedRange: "<1.10.1", | |
| 166 | fixedVersion: "1.10.1", | |
| 167 | referenceUrl: "https://github.com/advisories/GHSA-xx7p-3c2j-8c7w", | |
| 168 | }, | |
| 169 | { | |
| 170 | ghsaId: "GHSA-5545-jx63-4mgx", | |
| 171 | cveId: "CVE-2020-26160", | |
| 172 | summary: "JWT-go incorrect type assertion allows auth bypass", | |
| 173 | severity: "high", | |
| 174 | ecosystem: "go", | |
| 175 | packageName: "github.com/dgrijalva/jwt-go", | |
| 176 | affectedRange: "<4.0.0", | |
| 177 | fixedVersion: "4.0.0", | |
| 178 | referenceUrl: "https://github.com/advisories/GHSA-w73w-5m7g-f7qc", | |
| 179 | }, | |
| 180 | ]; | |
| 181 | ||
| 182 | // ---------------------------------------------------------------------------- | |
| 183 | // Version range matcher | |
| 184 | // ---------------------------------------------------------------------------- | |
| 185 | ||
| 186 | /** | |
| 187 | * Parse a dotted version string → numeric components. Non-numeric segments | |
| 188 | * become 0 so "1.2.3-beta" < "1.2.3". Good enough for advisory comparisons. | |
| 189 | */ | |
| 190 | export function parseVersion(v: string): number[] { | |
| 191 | const cleaned = (v || "").trim().replace(/^[\^~=v\s]+/, ""); | |
| 192 | if (!cleaned) return [0, 0, 0]; | |
| 193 | const stem = cleaned.split(/[+\-\s]/, 1)[0]; // strip prerelease / build | |
| 194 | return stem | |
| 195 | .split(".") | |
| 196 | .map((p) => parseInt(p, 10)) | |
| 197 | .map((n) => (Number.isFinite(n) ? n : 0)); | |
| 198 | } | |
| 199 | ||
| 200 | export function compareVersions(a: string, b: string): number { | |
| 201 | const av = parseVersion(a); | |
| 202 | const bv = parseVersion(b); | |
| 203 | const len = Math.max(av.length, bv.length); | |
| 204 | for (let i = 0; i < len; i++) { | |
| 205 | const x = av[i] ?? 0; | |
| 206 | const y = bv[i] ?? 0; | |
| 207 | if (x !== y) return x - y; | |
| 208 | } | |
| 209 | return 0; | |
| 210 | } | |
| 211 | ||
| 212 | /** Satisfies "<1.2.3", ">=1.0.0", ">=1.0 <2.0", "=1.2.3", or bare "1.2.3". */ | |
| 213 | export function satisfiesRange(version: string, range: string): boolean { | |
| 214 | const clauses = range | |
| 215 | .split(/\s+/) | |
| 216 | .map((c) => c.trim()) | |
| 217 | .filter(Boolean); | |
| 218 | if (clauses.length === 0) return false; | |
| 219 | for (const clause of clauses) { | |
| 220 | const m = clause.match(/^(<=|>=|<|>|=)?\s*(\S+)$/); | |
| 221 | if (!m) return false; | |
| 222 | const op = m[1] || "="; | |
| 223 | const target = m[2]; | |
| 224 | const cmp = compareVersions(version, target); | |
| 225 | let ok = false; | |
| 226 | switch (op) { | |
| 227 | case "<": | |
| 228 | ok = cmp < 0; | |
| 229 | break; | |
| 230 | case "<=": | |
| 231 | ok = cmp <= 0; | |
| 232 | break; | |
| 233 | case ">": | |
| 234 | ok = cmp > 0; | |
| 235 | break; | |
| 236 | case ">=": | |
| 237 | ok = cmp >= 0; | |
| 238 | break; | |
| 239 | case "=": | |
| 240 | ok = cmp === 0; | |
| 241 | break; | |
| 242 | } | |
| 243 | if (!ok) return false; | |
| 244 | } | |
| 245 | return true; | |
| 246 | } | |
| 247 | ||
| 248 | /** | |
| 249 | * Extract a concrete version from a manifest spec: | |
| 250 | * "^1.2.3" → "1.2.3" | |
| 251 | * "~2.0.0" → "2.0.0" | |
| 252 | * "v1.8.0" → "1.8.0" | |
| 253 | * ">=1.0 <2.0" → "1.0" (lower bound — conservative) | |
| 254 | * "1.2.3" → "1.2.3" | |
| 255 | * | |
| 256 | * Returns null if the spec has no concrete version (e.g. "*" or git URL). | |
| 257 | */ | |
| 258 | export function normalizeManifestVersion(spec: string | null): string | null { | |
| 259 | if (!spec) return null; | |
| 260 | const trimmed = spec.trim(); | |
| 261 | if (!trimmed || trimmed === "*" || trimmed === "latest") return null; | |
| 262 | // Grab the first number-like token | |
| 263 | const m = trimmed.match(/(\d+(?:\.\d+)*(?:\.\d+)?)/); | |
| 264 | if (!m) return null; | |
| 265 | return m[1]; | |
| 266 | } | |
| 267 | ||
| 268 | /** | |
| 269 | * True when a declared manifest spec overlaps with an advisory range. | |
| 270 | * When the spec can't be pinned to a version, we still return true to | |
| 271 | * surface the potential risk (false positives are safer than false | |
| 272 | * negatives in a dependabot-style feature). | |
| 273 | */ | |
| 274 | export function rangeMatches( | |
| 275 | manifestSpec: string | null, | |
| 276 | affectedRange: string | |
| 277 | ): boolean { | |
| 278 | const normalized = normalizeManifestVersion(manifestSpec); | |
| 279 | if (!normalized) { | |
| 280 | // Can't resolve spec → conservatively match (no concrete version means | |
| 281 | // the floating spec could resolve to anything). | |
| 282 | return true; | |
| 283 | } | |
| 284 | return satisfiesRange(normalized, affectedRange); | |
| 285 | } | |
| 286 | ||
| 287 | // ---------------------------------------------------------------------------- | |
| 288 | // Seeding | |
| 289 | // ---------------------------------------------------------------------------- | |
| 290 | ||
| 291 | /** | |
| 292 | * Inserts the hardcoded seed list if not already present, matched by | |
| 293 | * `ghsa_id`. Safe to call on every boot — idempotent. | |
| 294 | */ | |
| 295 | export async function seedAdvisories(): Promise<{ inserted: number }> { | |
| 296 | let inserted = 0; | |
| 297 | for (const a of SEED_ADVISORIES) { | |
| 298 | try { | |
| 299 | const [existing] = await db | |
| 300 | .select({ id: securityAdvisories.id }) | |
| 301 | .from(securityAdvisories) | |
| 302 | .where(eq(securityAdvisories.ghsaId, a.ghsaId)) | |
| 303 | .limit(1); | |
| 304 | if (existing) continue; | |
| 305 | await db.insert(securityAdvisories).values({ | |
| 306 | ghsaId: a.ghsaId, | |
| 307 | cveId: a.cveId ?? null, | |
| 308 | summary: a.summary, | |
| 309 | severity: a.severity, | |
| 310 | ecosystem: a.ecosystem, | |
| 311 | packageName: a.packageName, | |
| 312 | affectedRange: a.affectedRange, | |
| 313 | fixedVersion: a.fixedVersion ?? null, | |
| 314 | referenceUrl: a.referenceUrl ?? null, | |
| 315 | }); | |
| 316 | inserted++; | |
| 317 | } catch (err) { | |
| 318 | console.error("[advisories] seed:", err); | |
| 319 | } | |
| 320 | } | |
| 321 | return { inserted }; | |
| 322 | } | |
| 323 | ||
| 324 | // ---------------------------------------------------------------------------- | |
| 325 | // Scan + alert upsert | |
| 326 | // ---------------------------------------------------------------------------- | |
| 327 | ||
| 328 | export async function scanRepositoryForAlerts( | |
| 329 | repositoryId: string | |
| 330 | ): Promise<{ opened: number; matched: number; closed: number } | null> { | |
| 331 | try { | |
| 332 | const deps = await db | |
| 333 | .select() | |
| 334 | .from(repoDependencies) | |
| 335 | .where(eq(repoDependencies.repositoryId, repositoryId)); | |
| 336 | ||
| 337 | if (deps.length === 0) { | |
| 338 | // All prior alerts become fixed | |
| 339 | const closed = await closeAllAlerts(repositoryId); | |
| 340 | return { opened: 0, matched: 0, closed }; | |
| 341 | } | |
| 342 | ||
| 343 | const ecosystems = Array.from(new Set(deps.map((d) => d.ecosystem))); | |
| 344 | const advisories = await db | |
| 345 | .select() | |
| 346 | .from(securityAdvisories) | |
| 347 | .where(inArray(securityAdvisories.ecosystem, ecosystems)); | |
| 348 | ||
| 349 | const existing = await db | |
| 350 | .select() | |
| 351 | .from(repoAdvisoryAlerts) | |
| 352 | .where(eq(repoAdvisoryAlerts.repositoryId, repositoryId)); | |
| 353 | const existingByKey = new Map<string, RepoAdvisoryAlert>(); | |
| 354 | for (const e of existing) { | |
| 355 | existingByKey.set(`${e.advisoryId}::${e.manifestPath}`, e); | |
| 356 | } | |
| 357 | ||
| 358 | let opened = 0; | |
| 359 | let matched = 0; | |
| 360 | const keepKeys = new Set<string>(); | |
| 361 | ||
| 362 | for (const dep of deps) { | |
| 363 | for (const adv of advisories) { | |
| 364 | if (adv.ecosystem !== dep.ecosystem) continue; | |
| 365 | if (adv.packageName !== dep.name) continue; | |
| 366 | if (!rangeMatches(dep.versionSpec, adv.affectedRange)) continue; | |
| 367 | matched++; | |
| 368 | const key = `${adv.id}::${dep.manifestPath}`; | |
| 369 | keepKeys.add(key); | |
| 370 | const prior = existingByKey.get(key); | |
| 371 | if (prior) { | |
| 372 | // Reopen if previously fixed; keep dismissed as dismissed. | |
| 373 | if (prior.status === "fixed") { | |
| 374 | await db | |
| 375 | .update(repoAdvisoryAlerts) | |
| 376 | .set({ | |
| 377 | status: "open", | |
| 378 | dependencyVersion: dep.versionSpec ?? null, | |
| 379 | updatedAt: new Date(), | |
| 380 | }) | |
| 381 | .where(eq(repoAdvisoryAlerts.id, prior.id)); | |
| 382 | } else { | |
| 383 | await db | |
| 384 | .update(repoAdvisoryAlerts) | |
| 385 | .set({ | |
| 386 | dependencyVersion: dep.versionSpec ?? null, | |
| 387 | updatedAt: new Date(), | |
| 388 | }) | |
| 389 | .where(eq(repoAdvisoryAlerts.id, prior.id)); | |
| 390 | } | |
| 391 | } else { | |
| 392 | await db.insert(repoAdvisoryAlerts).values({ | |
| 393 | repositoryId, | |
| 394 | advisoryId: adv.id, | |
| 395 | dependencyName: dep.name, | |
| 396 | dependencyVersion: dep.versionSpec ?? null, | |
| 397 | manifestPath: dep.manifestPath, | |
| 398 | }); | |
| 399 | opened++; | |
| 400 | } | |
| 401 | } | |
| 402 | } | |
| 403 | ||
| 404 | // Close alerts whose dep + advisory combo is no longer present | |
| 405 | let closed = 0; | |
| 406 | for (const [key, prior] of existingByKey) { | |
| 407 | if (keepKeys.has(key)) continue; | |
| 408 | if (prior.status === "dismissed") continue; | |
| 409 | if (prior.status === "fixed") continue; | |
| 410 | await db | |
| 411 | .update(repoAdvisoryAlerts) | |
| 412 | .set({ status: "fixed", updatedAt: new Date() }) | |
| 413 | .where(eq(repoAdvisoryAlerts.id, prior.id)); | |
| 414 | closed++; | |
| 415 | } | |
| 416 | ||
| 417 | return { opened, matched, closed }; | |
| 418 | } catch (err) { | |
| 419 | console.error("[advisories] scanRepositoryForAlerts:", err); | |
| 420 | return null; | |
| 421 | } | |
| 422 | } | |
| 423 | ||
| 424 | async function closeAllAlerts(repositoryId: string): Promise<number> { | |
| 425 | try { | |
| 426 | const existing = await db | |
| 427 | .select() | |
| 428 | .from(repoAdvisoryAlerts) | |
| 429 | .where( | |
| 430 | and( | |
| 431 | eq(repoAdvisoryAlerts.repositoryId, repositoryId), | |
| 432 | eq(repoAdvisoryAlerts.status, "open") | |
| 433 | ) | |
| 434 | ); | |
| 435 | if (existing.length === 0) return 0; | |
| 436 | await db | |
| 437 | .update(repoAdvisoryAlerts) | |
| 438 | .set({ status: "fixed", updatedAt: new Date() }) | |
| 439 | .where( | |
| 440 | and( | |
| 441 | eq(repoAdvisoryAlerts.repositoryId, repositoryId), | |
| 442 | eq(repoAdvisoryAlerts.status, "open") | |
| 443 | ) | |
| 444 | ); | |
| 445 | return existing.length; | |
| 446 | } catch { | |
| 447 | return 0; | |
| 448 | } | |
| 449 | } | |
| 450 | ||
| 451 | export interface AlertWithAdvisory extends RepoAdvisoryAlert { | |
| 452 | advisory: SecurityAdvisory; | |
| 453 | } | |
| 454 | ||
| 455 | export async function listAlertsForRepo( | |
| 456 | repositoryId: string, | |
| 457 | status: "open" | "dismissed" | "fixed" | "all" = "open" | |
| 458 | ): Promise<AlertWithAdvisory[]> { | |
| 459 | try { | |
| 460 | const whereClauses = | |
| 461 | status === "all" | |
| 462 | ? eq(repoAdvisoryAlerts.repositoryId, repositoryId) | |
| 463 | : and( | |
| 464 | eq(repoAdvisoryAlerts.repositoryId, repositoryId), | |
| 465 | eq(repoAdvisoryAlerts.status, status) | |
| 466 | )!; | |
| 467 | const rows = await db | |
| 468 | .select({ | |
| 469 | alert: repoAdvisoryAlerts, | |
| 470 | advisory: securityAdvisories, | |
| 471 | }) | |
| 472 | .from(repoAdvisoryAlerts) | |
| 473 | .innerJoin( | |
| 474 | securityAdvisories, | |
| 475 | eq(securityAdvisories.id, repoAdvisoryAlerts.advisoryId) | |
| 476 | ) | |
| 477 | .where(whereClauses) | |
| 478 | .orderBy(desc(repoAdvisoryAlerts.createdAt)); | |
| 479 | return rows.map((r) => ({ ...r.alert, advisory: r.advisory })); | |
| 480 | } catch { | |
| 481 | return []; | |
| 482 | } | |
| 483 | } | |
| 484 | ||
| 485 | export async function dismissAlert( | |
| 486 | alertId: string, | |
| 487 | repositoryId: string, | |
| 488 | reason: string | |
| 489 | ): Promise<boolean> { | |
| 490 | try { | |
| 491 | const result = await db | |
| 492 | .update(repoAdvisoryAlerts) | |
| 493 | .set({ | |
| 494 | status: "dismissed", | |
| 495 | dismissedReason: reason.slice(0, 280), | |
| 496 | updatedAt: new Date(), | |
| 497 | }) | |
| 498 | .where( | |
| 499 | and( | |
| 500 | eq(repoAdvisoryAlerts.id, alertId), | |
| 501 | eq(repoAdvisoryAlerts.repositoryId, repositoryId) | |
| 502 | ) | |
| 503 | ) | |
| 504 | .returning({ id: repoAdvisoryAlerts.id }); | |
| 505 | return result.length > 0; | |
| 506 | } catch { | |
| 507 | return false; | |
| 508 | } | |
| 509 | } | |
| 510 | ||
| 511 | export async function reopenAlert( | |
| 512 | alertId: string, | |
| 513 | repositoryId: string | |
| 514 | ): Promise<boolean> { | |
| 515 | try { | |
| 516 | const result = await db | |
| 517 | .update(repoAdvisoryAlerts) | |
| 518 | .set({ | |
| 519 | status: "open", | |
| 520 | dismissedReason: null, | |
| 521 | updatedAt: new Date(), | |
| 522 | }) | |
| 523 | .where( | |
| 524 | and( | |
| 525 | eq(repoAdvisoryAlerts.id, alertId), | |
| 526 | eq(repoAdvisoryAlerts.repositoryId, repositoryId) | |
| 527 | ) | |
| 528 | ) | |
| 529 | .returning({ id: repoAdvisoryAlerts.id }); | |
| 530 | return result.length > 0; | |
| 531 | } catch { | |
| 532 | return false; | |
| 533 | } | |
| 534 | } | |
| 535 | ||
| 536 | // ---------------------------------------------------------------------------- | |
| 537 | // Test-only exports | |
| 538 | // ---------------------------------------------------------------------------- | |
| 539 | ||
| 540 | export const __internal = { | |
| 541 | parseVersion, | |
| 542 | compareVersions, | |
| 543 | satisfiesRange, | |
| 544 | normalizeManifestVersion, | |
| 545 | rangeMatches, | |
| 546 | }; |