Skip to content

Commit 2576aca

Browse files
authored
feat: expose security contacts via api [CM-1297] (#4304)
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 79aa52b commit 2576aca

5 files changed

Lines changed: 124 additions & 16 deletions

File tree

backend/src/api/public/v1/akrites/openapi.yaml

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,31 @@ components:
397397
type: integer
398398
description: Total packages marked as critical (is_critical = true).
399399

400+
SecurityContactConfidence:
401+
type: string
402+
enum: [PRIMARY, SECONDARY, FALLBACK, NONE]
403+
404+
SecurityContact:
405+
type: object
406+
required: [channel, value, role, confidence, score]
407+
properties:
408+
channel:
409+
type: string
410+
enum: [email, github-pvr, url, github-handle, web-form]
411+
value:
412+
type: string
413+
example: security@expressjs.com
414+
role:
415+
type: string
416+
enum: [security-team, maintainer, admin, committer, org-owner]
417+
confidence:
418+
$ref: '#/components/schemas/SecurityContactConfidence'
419+
score:
420+
type: number
421+
format: float
422+
minimum: 0
423+
maximum: 1
424+
400425
Advisory:
401426
type: object
402427
required: [osvId, severity, resolution, isCritical]
@@ -522,8 +547,32 @@ components:
522547
securityContacts:
523548
type: array
524549
nullable: true
550+
description: >-
551+
null when the linked repo has not yet been swept by the security-contacts
552+
pipeline; empty array when swept with no contacts found. Provenance and
553+
internal scoring metadata are never included.
525554
items:
526-
type: string
555+
$ref: '#/components/schemas/SecurityContact'
556+
packageConfidence:
557+
allOf:
558+
- $ref: '#/components/schemas/SecurityContactConfidence'
559+
nullable: true
560+
description: Confidence band of the highest-scoring contact in securityContacts.
561+
securityPolicies:
562+
type: object
563+
properties:
564+
securityPolicyUrl:
565+
type: string
566+
nullable: true
567+
vulnerabilityReportingUrl:
568+
type: string
569+
nullable: true
570+
bugBountyUrl:
571+
type: string
572+
nullable: true
573+
pvrEnabled:
574+
type: boolean
575+
nullable: true
527576
advisories:
528577
type: array
529578
items:

backend/src/api/public/v1/packages/getPackage.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getAdvisoriesByPackageId,
77
getPackageDetailByPurl,
88
getStewardshipSummary,
9+
securityContactConfidenceBand,
910
} from '@crowd/data-access-layer'
1011

1112
import { getPackagesQx } from '@/db/packagesDb'
@@ -50,6 +51,21 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
5051
const mappingConfidence =
5152
pkg.repoMappingConfidence != null ? Number(pkg.repoMappingConfidence) : null
5253

54+
const securityContacts =
55+
pkg.contactsLastRefreshed == null
56+
? null
57+
: (pkg.securityContacts ?? []).map((c) => ({
58+
channel: c.channel,
59+
value: c.value,
60+
role: c.role,
61+
confidence: c.confidence,
62+
score: Number(c.score),
63+
}))
64+
const packageConfidence =
65+
securityContacts && securityContacts.length > 0
66+
? securityContactConfidenceBand(Math.max(...securityContacts.map((c) => c.score)))
67+
: null
68+
5369
ok(res, {
5470
purl: pkg.purl,
5571
name: pkg.name,
@@ -92,15 +108,22 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
92108
signalCoverageHealth: snakeToCamelKeys(pkg.signalCoverageHealth),
93109
assessment: null,
94110
security: {
95-
securityContacts: null,
111+
securityContacts,
112+
packageConfidence,
113+
securityPolicies: {
114+
securityPolicyUrl: pkg.securityPolicyUrl ?? null,
115+
vulnerabilityReportingUrl: pkg.vulnerabilityReportingUrl ?? null,
116+
bugBountyUrl: pkg.bugBountyUrl ?? null,
117+
pvrEnabled: pkg.pvrEnabled ?? null,
118+
},
96119
advisories: advisories.map((a) => ({
97120
osvId: a.osvId,
98121
severity: a.severity,
99122
resolution: a.resolution,
100123
isCritical: a.isCritical,
101124
})),
102125
cvd: {
103-
isPvrEnabled: null,
126+
isPvrEnabled: pkg.pvrEnabled ?? null,
104127
tier0Steward: null,
105128
criticalVulnerabilityFlag: pkg.hasCriticalVulnerability,
106129
},

services/apps/packages_worker/src/security-contacts/score.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ConfidenceBand, ContactChannel, ProvenanceEntry, RawContact, SourceTier } from './types'
1+
import {
2+
SecurityContactConfidence,
3+
securityContactConfidenceBand,
4+
} from '@crowd/data-access-layer/src/osspckgs/api'
5+
6+
import { ContactChannel, ProvenanceEntry, RawContact, SourceTier } from './types'
27

38
const WEIGHTS = { tier: 0.55, channel: 0.2, freshness: 0.15, corroboration: 0.1 }
49

@@ -84,17 +89,10 @@ function corroborationScore(provenance: ProvenanceEntry[]): number {
8489
return 0
8590
}
8691

87-
export function confidenceBand(score: number): ConfidenceBand {
88-
if (score >= 0.8) return 'PRIMARY'
89-
if (score >= 0.55) return 'SECONDARY'
90-
if (score >= 0.3) return 'FALLBACK'
91-
return 'NONE'
92-
}
93-
9492
export function scoreContact(
9593
contact: RawContact,
9694
now: Date = new Date(),
97-
): { score: number; confidence: ConfidenceBand } {
95+
): { score: number; confidence: SecurityContactConfidence } {
9896
const raw =
9997
WEIGHTS.tier * TIER_SCORE[contact.tier] +
10098
WEIGHTS.channel * channelQuality(contact.channel, contact.value) +
@@ -103,5 +101,5 @@ export function scoreContact(
103101
(contact.channel === 'github-handle' ? HANDLE_ONLY_PENALTY : 0)
104102

105103
const score = Math.round(Math.min(1, Math.max(0, raw)) * 1000) / 1000
106-
return { score, confidence: confidenceBand(score) }
104+
return { score, confidence: securityContactConfidenceBand(score) }
107105
}

services/apps/packages_worker/src/security-contacts/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import type { SecurityContactConfidence } from '@crowd/data-access-layer/src/osspckgs/api'
2+
13
export type ContactChannel = 'email' | 'github-pvr' | 'url' | 'github-handle' | 'web-form'
24

35
export type ContactRole = 'security-team' | 'maintainer' | 'admin' | 'committer' | 'org-owner'
46

5-
export type ConfidenceBand = 'PRIMARY' | 'SECONDARY' | 'FALLBACK' | 'NONE'
6-
77
export type SourceTier = 'A' | 'B' | 'C' | 'D'
88

99
/** Where a single contact value was observed, for auditability and corroboration scoring. */
@@ -29,7 +29,7 @@ export interface RawContact {
2929

3030
export interface ScoredContact extends RawContact {
3131
score: number
32-
confidence: ConfidenceBand
32+
confidence: SecurityContactConfidence
3333
}
3434

3535
export interface RepoPolicies {

services/libs/data-access-layer/src/osspckgs/api.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,16 @@ export async function listPackagesForApi(
595595
return { rows, total }
596596
}
597597

598+
export type SecurityContactConfidence = 'PRIMARY' | 'SECONDARY' | 'FALLBACK' | 'NONE'
599+
600+
export interface SecurityContactRow {
601+
channel: string
602+
value: string
603+
role: string
604+
confidence: SecurityContactConfidence
605+
score: number
606+
}
607+
598608
export interface PackageDetailRow {
599609
id: string
600610
purl: string
@@ -625,6 +635,12 @@ export interface PackageDetailRow {
625635
hasSecurityFile: boolean | null
626636
hasSecurityPolicy: boolean | null
627637
branchProtectionEnabled: boolean | null
638+
pvrEnabled: boolean | null
639+
securityPolicyUrl: string | null
640+
vulnerabilityReportingUrl: string | null
641+
bugBountyUrl: string | null
642+
contactsLastRefreshed: Date | null
643+
securityContacts: SecurityContactRow[] | null
628644
// from downloads_last_30d
629645
downloadsLast30d: string | null
630646
maintainerCount: number
@@ -639,6 +655,13 @@ export interface PackageDetailRow {
639655
signalCoverageHealth: Record<string, unknown> | null
640656
}
641657

658+
export function securityContactConfidenceBand(score: number): SecurityContactConfidence {
659+
if (score >= 0.8) return 'PRIMARY'
660+
if (score >= 0.55) return 'SECONDARY'
661+
if (score >= 0.3) return 'FALLBACK'
662+
return 'NONE'
663+
}
664+
642665
export interface AdvisoryRow {
643666
osvId: string
644667
severity: string
@@ -682,6 +705,21 @@ export async function getPackageDetailByPurl(
682705
r.security_file_enabled AS "hasSecurityFile",
683706
r.security_policy_enabled AS "hasSecurityPolicy",
684707
r.branch_protection_enabled AS "branchProtectionEnabled",
708+
r.pvr_enabled AS "pvrEnabled",
709+
r.security_policy_url AS "securityPolicyUrl",
710+
r.vulnerability_reporting_url AS "vulnerabilityReportingUrl",
711+
r.bug_bounty_url AS "bugBountyUrl",
712+
r.contacts_last_refreshed AS "contactsLastRefreshed",
713+
(
714+
SELECT json_agg(sc ORDER BY sc.score DESC)
715+
FROM (
716+
SELECT channel, value, role, confidence, score
717+
FROM security_contacts
718+
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
719+
ORDER BY score DESC
720+
LIMIT 5
721+
) sc
722+
) AS "securityContacts",
685723
-- latest 30-day download count
686724
(
687725
SELECT d.count::text

0 commit comments

Comments
 (0)