Skip to content

Commit 4ed8143

Browse files
authored
Merge branch 'main' into feat/CM-361-part-1
2 parents e0db545 + 2576aca commit 4ed8143

11 files changed

Lines changed: 146 additions & 33 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
},

backend/src/database/repositories/memberRepository.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ class MemberRepository {
244244

245245
const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments)
246246

247+
if (subprojectIds.length === 0) {
248+
return
249+
}
250+
247251
await seq.query(bulkDeleteMemberSegments, {
248252
replacements: {
249253
memberIds,

backend/src/database/repositories/organizationRepository.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ class OrganizationRepository {
192192
const qx = SequelizeRepository.getQueryExecutor(options)
193193
const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments)
194194

195+
if (subprojectIds.length === 0) {
196+
return
197+
}
198+
195199
await seq.query(bulkDeleteOrganizationSegments, {
196200
replacements: {
197201
organizationIds,

services/apps/packages_worker/src/nuget/normalize.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ function isPrerelease(version: string): boolean {
1919
return version.includes('-')
2020
}
2121

22+
// NuGet stamps unlisted versions with 1900-01-01T00:00:00Z as a sentinel — treat as absent.
23+
function parsePublishedDate(published: string | undefined): Date | null {
24+
if (!published) return null
25+
const date = new Date(published)
26+
return !isNaN(date.getTime()) && date.getUTCFullYear() > 1900 ? date : null
27+
}
28+
2229
const SCM_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org']
2330

2431
function normalizeRepoUrl(url: string | undefined): string | null {
@@ -105,25 +112,15 @@ export function normalizeNuGetPackage(
105112
status = 'active'
106113
}
107114

108-
// NuGet stamps unlisted versions with 1900-01-01T00:00:00Z as a sentinel — exclude them.
109115
const publishedDates = allEntries
110-
.filter((e) => e.published)
111-
.map((e) => new Date(e.published as string))
112-
.filter((d) => !isNaN(d.getTime()) && d.getUTCFullYear() > 1900)
116+
.map((e) => parsePublishedDate(e.published))
117+
.filter((d): d is Date => d !== null)
113118
.sort((a, b) => a.getTime() - b.getTime())
114119

115120
const firstReleaseAt = publishedDates.length > 0 ? publishedDates[0] : null
116121

117122
const latestEntry4Date = latestListedEntry ?? latestEntry
118-
const latestReleaseAtRaw = latestEntry4Date?.published
119-
? new Date(latestEntry4Date.published)
120-
: null
121-
const latestReleaseAt =
122-
latestReleaseAtRaw &&
123-
!isNaN(latestReleaseAtRaw.getTime()) &&
124-
latestReleaseAtRaw.getUTCFullYear() > 1900
125-
? latestReleaseAtRaw
126-
: null
123+
const latestReleaseAt = parsePublishedDate(latestEntry4Date?.published)
127124

128125
const totalDownloads = searchResult?.totalDownloads ?? 0
129126

@@ -144,7 +141,7 @@ export function normalizeNuGetPackage(
144141
const { licenses: vLicenses } = parseLicense(entry.licenseExpression, entry.licenseUrl)
145142
return {
146143
number: ver,
147-
publishedAt: entry.published ? new Date(entry.published) : null,
144+
publishedAt: parsePublishedDate(entry.published),
148145
isLatest: ver === latestVersion,
149146
isPrerelease: isPrerelease(ver),
150147
isYanked: entry.listed === false,

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/apps/pcc_sync_worker/src/activities/exportActivity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function buildSourceQuery(): string {
2424
p.description,
2525
p.project_logo,
2626
p.project_status,
27-
p.project_maturity_level,
27+
p.project_admin_category,
2828
ps.mapped_project_id,
2929
ps.mapped_project_name,
3030
ps.mapped_project_slug,

services/apps/pcc_sync_worker/src/parser/rowParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export function parsePccRow(rawRows: Record<string, unknown>[]): ParseResult {
180180
pccSlug: leafSlug,
181181
name,
182182
status: mappedStatus,
183-
maturity: trimOrNull(firstRaw.PROJECT_MATURITY_LEVEL),
183+
maturity: trimOrNull(firstRaw.PROJECT_ADMIN_CATEGORY),
184184
description: trimOrNull(firstRaw.DESCRIPTION),
185185
logoUrl: trimOrNull(firstRaw.PROJECT_LOGO),
186186
segmentIdFromSnowflake: trimOrNull(firstRaw.SEGMENT_ID),

services/apps/pcc_sync_worker/src/parser/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface PccParquetRow {
1919
DESCRIPTION: string | null
2020
PROJECT_LOGO: string | null
2121
PROJECT_STATUS: string | null
22-
PROJECT_MATURITY_LEVEL: string | null
22+
PROJECT_ADMIN_CATEGORY: string | null
2323
/** ID of the ancestor at this hierarchy level (hierarchy_level=1 → leaf itself). */
2424
MAPPED_PROJECT_ID: string | null
2525
/** Name of the ancestor at this hierarchy level. */

0 commit comments

Comments
 (0)