Skip to content

Commit befa6d6

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/advisory-packages-bq-ceiling-CM-1362
2 parents f96972a + 4a7d9e4 commit befa6d6

38 files changed

Lines changed: 2540 additions & 126 deletions

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ info:
1111
1212
Packages, Advisories and Contacts endpoints are implemented. Blast Radius
1313
submit (2a) and poll (2b) are both implemented, backed by a 4-stage
14-
Temporal pipeline (intel, dependents, reachability, report) for npm, go, and
15-
maven packages; other ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED. The
14+
Temporal pipeline (intel, dependents, reachability, report) for npm, go, maven, and
15+
cargo packages; other ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED. The
1616
7-day result cache is specced separately and not yet built.
1717
1818
@@ -61,7 +61,7 @@ tags:
6161
description: >
6262
Advisory reachability analysis — submit (2a) and poll (2b) are both
6363
implemented, each with a bulk counterpart. Submitting kicks off a
64-
Temporal workflow that runs the npm, go, or maven reachability pipeline (other
64+
Temporal workflow that runs the npm, go, maven, or cargo reachability pipeline (other
6565
ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED); poll returns job
6666
status and, once done, results. Bulk submit (jobs:batch) is capped at
6767
20 jobs per request (10 recommended as the default batch size) — each
@@ -411,9 +411,9 @@ components:
411411
example: GHSA-jf85-cpcp-j695
412412
ecosystem:
413413
type: string
414-
enum: [npm, go, maven]
414+
enum: [npm, go, maven, cargo]
415415
description: >
416-
Required. The reachability pipeline supports npm, go, and maven today —
416+
Required. The reachability pipeline supports npm, go, maven, and cargo today —
417417
any other value, or a missing ecosystem, is rejected with a 400.
418418
example: npm
419419
package:
@@ -450,7 +450,7 @@ components:
450450
description: Echoes the request's package. Null when the request omitted it.
451451
ecosystem:
452452
type: string
453-
enum: [npm, go, maven]
453+
enum: [npm, go, maven, cargo]
454454
description: Echoes the request's ecosystem. Always present — the request requires it.
455455
status:
456456
type: string
@@ -611,7 +611,7 @@ components:
611611
nullable: true
612612
ecosystem:
613613
type: string
614-
enum: [npm, go, maven]
614+
enum: [npm, go, maven, cargo]
615615
submittedAt:
616616
type: string
617617
nullable: true
@@ -1244,7 +1244,7 @@ paths:
12441244
advisories); provide it to narrow to one package, which is required
12451245
for advisories affecting more than one. Starts a Temporal workflow
12461246
running the 4-stage reachability pipeline
1247-
(intel, dependents, reachability, report) for npm, go, or maven; other
1247+
(intel, dependents, reachability, report) for npm, go, maven, or cargo; other
12481248
ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED. Poll status/results
12491249
via GET /jobs/{analysisId}.
12501250
@@ -1272,7 +1272,7 @@ paths:
12721272
'400':
12731273
description: >
12741274
Validation error (missing/empty advisoryId), or an unsupported
1275-
ecosystem — only npm, go, and maven are supported today, so any other
1275+
ecosystem — only npm, go, maven, and cargo are supported today, so any other
12761276
value (including a missing ecosystem) is rejected before a
12771277
workflow is started.
12781278
content:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from 'zod'
22

3-
export const SUPPORTED_BLAST_RADIUS_ECOSYSTEMS = ['npm', 'go', 'maven'] as const
3+
export const SUPPORTED_BLAST_RADIUS_ECOSYSTEMS = ['npm', 'go', 'maven', 'cargo', 'nuget'] as const
44

55
// Always exactly one job per request — advisory-wide (package omitted) or narrowed
66
// to a single package. package accepts either a full purl or a bare package name,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ function toPurl(ecosystem: BlastRadiusJobEcosystem, name: string): string {
6363
}
6464
case 'go':
6565
return `pkg:golang/${name}`
66+
case 'cargo':
67+
return `pkg:cargo/${name}`
6668
case 'npm':
6769
default:
6870
return `pkg:npm/${name.replace(/^@/, '%40')}`

services/apps/packages_worker/src/blast-radius/__tests__/ecosystemSupport.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { ApplicationFailure } from '@temporalio/workflow'
22
import { describe, expect, it } from 'vitest'
33

4-
import { buildEcosystemNotSupportedFailure } from '../ecosystemSupport'
4+
import { SUPPORTED_ECOSYSTEMS, buildEcosystemNotSupportedFailure } from '../ecosystemSupport'
5+
6+
describe('SUPPORTED_ECOSYSTEMS', () => {
7+
it('includes cargo and nuget alongside npm, go, and maven', () => {
8+
expect(SUPPORTED_ECOSYSTEMS).toEqual(['npm', 'go', 'maven', 'cargo', 'nuget'])
9+
})
10+
})
511

612
describe('buildEcosystemNotSupportedFailure', () => {
713
it('builds a non-retryable ApplicationFailure tagged ECOSYSTEM_NOT_SUPPORTED', () => {

services/apps/packages_worker/src/blast-radius/__tests__/packageIdentifier.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { toBareNpmName } from '../packageIdentifier'
3+
import { toBareNpmName, toBareNuGetId, toDbCargoName } from '../packageIdentifier'
44

55
describe('toBareNpmName', () => {
66
it('returns a bare name unchanged', () => {
@@ -31,3 +31,41 @@ describe('toBareNpmName', () => {
3131
expect(toBareNpmName('pkg:npm/lodash@4.17.21?foo=bar#sub')).toBe('lodash')
3232
})
3333
})
34+
35+
describe('toDbCargoName', () => {
36+
it('leaves an already-underscored name unchanged', () => {
37+
expect(toDbCargoName('serde_json')).toBe('serde_json')
38+
})
39+
40+
it('converts hyphens to underscores, matching packages.name for hyphenated crates', () => {
41+
expect(toDbCargoName('serde-json')).toBe('serde_json')
42+
})
43+
44+
it('lowercases mixed-case names', () => {
45+
expect(toDbCargoName('Actix-Web')).toBe('actix_web')
46+
})
47+
})
48+
49+
describe('toBareNuGetId', () => {
50+
it('returns a bare id unchanged, preserving casing', () => {
51+
expect(toBareNuGetId('Newtonsoft.Json')).toBe('Newtonsoft.Json')
52+
})
53+
54+
it('strips the pkg:nuget/ prefix', () => {
55+
expect(toBareNuGetId('pkg:nuget/Newtonsoft.Json')).toBe('Newtonsoft.Json')
56+
})
57+
58+
it('strips a trailing version', () => {
59+
expect(toBareNuGetId('pkg:nuget/Newtonsoft.Json@13.0.1')).toBe('Newtonsoft.Json')
60+
})
61+
62+
it('strips qualifiers and subpath', () => {
63+
expect(toBareNuGetId('pkg:nuget/Newtonsoft.Json@13.0.1?foo=bar#sub')).toBe('Newtonsoft.Json')
64+
})
65+
66+
it('does not lowercase the id — DB lookups are case-sensitive', () => {
67+
expect(toBareNuGetId('pkg:nuget/Microsoft.AspNetCore.Mvc@2.2.0')).toBe(
68+
'Microsoft.AspNetCore.Mvc',
69+
)
70+
})
71+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { CARGO_INTEL_SCHEMA } from '../cargoPrompts'
4+
5+
describe('CARGO_INTEL_SCHEMA', () => {
6+
it('keeps import_signatures.properties keys in sync with its required list', () => {
7+
const importSignatures = CARGO_INTEL_SCHEMA.properties.import_signatures
8+
const propertyKeys = Object.keys(importSignatures.properties).sort()
9+
const requiredKeys = [...importSignatures.required].sort()
10+
expect(propertyKeys).toEqual(requiredKeys)
11+
})
12+
13+
it('keeps the top-level schema properties in sync with its required list', () => {
14+
const propertyKeys = Object.keys(CARGO_INTEL_SCHEMA.properties).sort()
15+
const requiredKeys = [...CARGO_INTEL_SCHEMA.required].sort()
16+
expect(propertyKeys).toEqual(requiredKeys)
17+
})
18+
})
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Parallels goPrompts.ts — schema shape and the intel prompt builder are shared via
2+
// promptKit.ts; only the Rust-specific keys/enum and system-prompt prose live here.
3+
import {
4+
buildIntelPrompt,
5+
buildIntelSchema,
6+
buildReachabilitySymbolsBlock,
7+
buildVerdictSchema,
8+
} from './promptKit'
9+
import { SymbolSpec } from './prompts'
10+
11+
// ---------- STAGE 1: INTEL ----------
12+
13+
const IMPORT_SIGNATURE_KEYS = [
14+
'use_path',
15+
'extern_crate',
16+
'macro_invocation',
17+
'fully_qualified_path',
18+
]
19+
20+
export const CARGO_INTEL_SCHEMA = buildIntelSchema(IMPORT_SIGNATURE_KEYS)
21+
22+
export const CARGO_INTEL_SYSTEM_PROMPT = `You are a vulnerability analyst. Your working directory contains the FULL SOURCE of the
23+
vulnerable version of a Rust crate. You are given the security advisory and the patch
24+
(diff) that fixed the vulnerability.
25+
26+
Your job is to determine, precisely, WHAT is vulnerable — so that downstream analysts can
27+
check whether other crates actually reach the vulnerable code.
28+
29+
Rules:
30+
- Identify the exact vulnerable function(s)/method(s)/type(s)/macro(s) from the patch and
31+
the source. Be minimal and precise: do NOT include similar-but-unaffected symbols. If the
32+
patch only touches a private (non-\`pub\`) helper, trace which \`pub\` symbols route through
33+
it and list those as the reachable surface (note the helper in \`notes\`).
34+
- Read the crate source to verify how each vulnerable symbol is exported — only items
35+
marked \`pub\` (or \`pub(crate)\`/\`pub(super)\`, which are NOT reachable from other crates)
36+
are visible outside the crate; note the exact module path each symbol lives in (e.g.
37+
\`crate::foo::bar\`), and whether it's re-exported elsewhere via \`pub use\`.
38+
- Build \`import_signatures\`: concrete code patterns a dependent crate would contain if it
39+
uses the vulnerable symbol. Cover: a plain \`use crate_name::path::Symbol\` followed by bare
40+
\`Symbol\` usage, an \`extern crate crate_name;\` (2018-edition-and-earlier style) followed by
41+
fully-qualified use, invocation of a vulnerable macro (\`crate_name::macro_name!(...)\` or
42+
\`use\`d then bare \`macro_name!(...)\`), and a fully-qualified path call
43+
(\`crate_name::path::Symbol::method(...)\`) without any \`use\`. These are the patterns
44+
analysts will grep for — make them literal and greppable, not prose.
45+
- \`reachability_notes\` must state what does NOT count (e.g. sibling functions that look
46+
similar but are not affected, usage confined to \`tests/\`, \`examples/\`, or code behind
47+
\`#[cfg(test)]\`) and any conditions required for exploitability (e.g. a specific Cargo
48+
feature flag must be enabled).
49+
- Set \`confidence\` for your identification: 0.9+ only if the patch unambiguously
50+
identifies the symbol(s); lower if you had to infer from indirect evidence.`
51+
52+
export const buildCargoIntelPrompt = buildIntelPrompt
53+
54+
// ---------- STAGE 3: REACHABILITY ----------
55+
56+
const IMPORT_STYLE_ENUM = [
57+
'use-path',
58+
'extern-crate',
59+
'macro-invocation',
60+
'fully-qualified-path',
61+
'reexport',
62+
'none',
63+
]
64+
65+
export const CARGO_VERDICT_SCHEMA = buildVerdictSchema(IMPORT_STYLE_ENUM)
66+
67+
export function buildCargoReachabilitySystemPrompt(spec: SymbolSpec): string {
68+
const { symbolsText, signatures } = buildReachabilitySymbolsBlock(spec)
69+
70+
return `You are a security reachability analyst. Your working directory contains the published
71+
source of ONE Rust crate (the "dependent") that declares a dependency on
72+
\`${spec.package}\`, which has a known vulnerability (${spec.vuln_id}).
73+
74+
## The vulnerability
75+
${spec.summary}
76+
77+
Vulnerable symbol(s) in \`${spec.package}\`:
78+
${symbolsText}
79+
80+
Exploit preconditions: ${spec.exploit_preconditions}
81+
82+
Analyst notes: ${spec.reachability_notes}
83+
84+
## Import signatures to look for
85+
${signatures}
86+
87+
## Your task
88+
Decide whether THIS dependent's own code actually reaches the vulnerable symbol(s).
89+
90+
Scope rules — follow strictly:
91+
1. Only the dependent's OWN shipped code counts (\`src/\`). Usage of the vulnerable symbol
92+
inside the dependent's OTHER dependencies (its own \`Cargo.toml\` deps) is OUT OF SCOPE
93+
(that is second-level analysis, done separately).
94+
2. Merely declaring a dependency on \`${spec.package}\` (present in \`Cargo.toml\`) is NOT
95+
enough — the vulnerable symbol itself must be reached. Uses of other items from the
96+
crate are irrelevant.
97+
3. Usage only in \`tests/\`, \`examples/\`, \`benches/\`, or code gated behind \`#[cfg(test)]\`
98+
that is not part of the shipped runtime code → \`not_affected\` (explain in reasoning).
99+
4. If the dependent RE-EXPORTS the vulnerable symbol to its own consumers (\`pub use\`, or a
100+
thin wrapper function/type that passes arguments through), that DOES count as \`affected\`
101+
with \`import_style: "reexport"\` — it propagates the vulnerable surface.
102+
5. Watch for indirect reachability inside the dependent's own code: fully-qualified paths
103+
(\`crate_name::module::Symbol\`), trait method calls through a re-exported trait, macro
104+
invocations, and generic/dyn dispatch through the vulnerable type.
105+
6. \`import_style\` describes how the VULNERABLE SYMBOL is reached, not how the crate is
106+
declared: report \`none\` whenever the vulnerable symbol itself is not reached, even if
107+
the crate is a dependency for other functionality.
108+
109+
Method: grep for the import signatures (and the bare symbol/macro names) across the source,
110+
open every hit, and trace whether the symbol is actually invoked. Check \`Cargo.toml\` to
111+
confirm the declared dependency, its version requirement, and whether any feature flags
112+
gate the vulnerable code path. Exclude \`tests/\`, \`examples/\`, \`benches/\`, and
113+
\`#[cfg(test)]\`-gated code from consideration.
114+
115+
## Confidence calibration
116+
- 0.8–1.0: direct evidence — you found (or ruled out) the import AND the call site
117+
explicitly; source was readable.
118+
- 0.4–0.8: symbol is imported but the call path is ambiguous (trait dispatch, conditional
119+
compilation, generated/macro-expanded code).
120+
- <0.4 and/or \`unclear\`: source is generated/absent, or indirection you could not resolve.
121+
122+
Report evidence as exact file paths, line numbers, and short verbatim snippets.`
123+
}
124+
125+
export const CARGO_REACHABILITY_PROMPT =
126+
'Analyze this crate per your instructions and produce the structured verdict. ' +
127+
'Start by listing the crate structure and grepping for the import signatures.'

0 commit comments

Comments
 (0)