Skip to content

Commit da20001

Browse files
committed
限制 Gradle 漏洞报告累计资源并补齐协议验收证据
1 parent 6912cb0 commit da20001

50 files changed

Lines changed: 2529 additions & 13 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/codeguard-cli/src/gradle_dependency_check_probe.rs‎

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,15 @@ pub fn observe(selected: &Request, cancelled: &AtomicBool) -> Value {
289289
};
290290
report["ownership_report_sha256"] = json!(digest(&ownership_bytes));
291291
let mut reports = Vec::new();
292+
let mut budget = crate::gradle_owasp_report_budget::GradleOwaspReportBudget::default();
292293
for owner in ownership {
294+
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
295+
fail!("request_cancelled");
296+
}
297+
if Instant::now() >= request.deadline {
298+
fail!("request_deadline_exceeded");
299+
}
300+
293301
// 原输出不能来自选定输入,且逐级物理路径不得通过符号链接逃逸私有副本。
294302
if snapshot
295303
.files()
@@ -304,7 +312,21 @@ pub fn observe(selected: &Request, cancelled: &AtomicBool) -> Value {
304312
if !canonical.starts_with(&project) || canonical != path {
305313
fail!("gradle_owasp_report_scope_invalid");
306314
}
307-
let Ok(bytes) = read_bounded_regular_file(&path, 8 * 1024 * 1024) else {
315+
let limit = budget.remaining_input().min(8 * 1024 * 1024);
316+
if limit == 0 {
317+
fail!("gradle_owasp_report_budget_exceeded");
318+
}
319+
if fs::symlink_metadata(&path)
320+
.ok()
321+
.is_some_and(|metadata| metadata.len() > limit as u64)
322+
{
323+
fail!(if limit < 8 * 1024 * 1024 {
324+
"gradle_owasp_report_budget_exceeded"
325+
} else {
326+
"gradle_owasp_report_unavailable"
327+
});
328+
}
329+
let Ok(bytes) = read_bounded_regular_file(&path, limit as u64) else {
308330
fail!("gradle_owasp_report_unavailable");
309331
};
310332
let Ok(parsed) = parse_owasp_dependency_check_json(&bytes) else {
@@ -313,8 +335,31 @@ pub fn observe(selected: &Request, cancelled: &AtomicBool) -> Value {
313335
if parsed.engine_version != "12.1.0" || parsed.project_name != owner.report_project_name {
314336
fail!("gradle_owasp_native_report_attribution_unresolved");
315337
}
316-
let advisories=parsed.advisories.iter().map(|item|json!({"source":item.source,"advisory_id":item.advisory_id,"score":item.score,"package_ids":item.package_ids,"dependency_sha256":item.dependency_sha256,"suppressed_by_native_tool":item.suppressed_by_native_tool})).collect::<Vec<_>>();
317-
reports.push(json!({"project_path":owner.project_path,"task_path":owner.task_path,"implementation":owner.implementation,"report_path":owner.report_path,"report_sha256":digest(&bytes),"engine_version":parsed.engine_version,"project_name":parsed.project_name,"report_date":parsed.report_date,"data_sources":parsed.data_sources,"dependency_count":parsed.dependency_count,"advisories":advisories}));
338+
if let Err(reason) = budget.reserve_input(bytes.len(), parsed.advisories.len()) {
339+
fail!(reason);
340+
}
341+
let mut row = json!({"project_path":owner.project_path,"task_path":owner.task_path,"implementation":owner.implementation,"report_path":owner.report_path,"report_sha256":digest(&bytes),"engine_version":parsed.engine_version,"project_name":parsed.project_name,"report_date":parsed.report_date,"data_sources":parsed.data_sources,"dependency_count":parsed.dependency_count,"advisories":[]});
342+
if let Err(reason) = budget.reserve_feedback(&row) {
343+
fail!(reason);
344+
}
345+
let mut advisories = Vec::new();
346+
for item in parsed.advisories {
347+
if cancelled.load(Ordering::Relaxed)
348+
|| codeguard_runtime::sigint_cancellation_requested()
349+
{
350+
fail!("request_cancelled");
351+
}
352+
if Instant::now() >= request.deadline {
353+
fail!("request_deadline_exceeded");
354+
}
355+
let value = json!({"source":item.source,"advisory_id":item.advisory_id,"score":item.score,"package_ids":item.package_ids,"dependency_sha256":item.dependency_sha256,"suppressed_by_native_tool":item.suppressed_by_native_tool});
356+
if let Err(reason) = budget.reserve_feedback(&value) {
357+
fail!(reason);
358+
}
359+
advisories.push(value);
360+
}
361+
row["advisories"] = json!(advisories);
362+
reports.push(row);
318363
}
319364
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
320365
fail!("request_cancelled");
@@ -410,5 +455,5 @@ fn private_scratch() -> Result<Scratch, ()> {
410455

411456
/// 构造缺前置的观察;不表示原生执行或漏洞清洁。
412457
pub fn missing_prerequisites() -> Value {
413-
json!({"schema_version":"0.1.0","report_type":"gradle_dependency_check_probe","checker_id":"java.gradle.dependency_check","native_status":"incomplete","reason":"prerequisites_missing","init_scripts_sha256":digest(format!("{MODEL_SCRIPT}\n{OWASP_SCRIPT}").as_bytes()),"dependency_cache_sha256":null,"model_report_sha256":null,"ownership_report_sha256":null,"source_snapshot_sha256":null,"gradle_bundle_sha256":null,"java_entry_sha256":null,"jdk_release_sha256":null,"task_paths":[],"native_exit_code":null,"native_stdout_sha256":null,"native_stderr_sha256":null,"reports":[],"database_freshness_verified":false,"dependency_attribution_verified":false,"coverage_proven":false,"authority":"local_unverified","delivery_decision":"not_evaluated"})
458+
json!({"schema_version":"0.2.0","report_type":"gradle_dependency_check_probe","checker_id":"java.gradle.dependency_check","native_status":"incomplete","reason":"prerequisites_missing","init_scripts_sha256":digest(format!("{MODEL_SCRIPT}\n{OWASP_SCRIPT}").as_bytes()),"dependency_cache_sha256":null,"model_report_sha256":null,"ownership_report_sha256":null,"source_snapshot_sha256":null,"gradle_bundle_sha256":null,"java_entry_sha256":null,"jdk_release_sha256":null,"task_paths":[],"native_exit_code":null,"native_stdout_sha256":null,"native_stderr_sha256":null,"reports":[],"database_freshness_verified":false,"dependency_attribution_verified":false,"coverage_proven":false,"authority":"local_unverified","delivery_decision":"not_evaluated"})
414459
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use serde_json::Value;
2+
3+
const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024;
4+
const MAX_ADVISORIES: usize = 1000;
5+
const MAX_FEEDBACK_BYTES: usize = 2 * 1024 * 1024;
6+
const EXCEEDED: &str = "gradle_owasp_report_budget_exceeded";
7+
8+
/// 单次Gradle执行的累计报告预算;防止多任务及重复包标识扩大内存和反馈。
9+
#[derive(Default)]
10+
pub(crate) struct GradleOwaspReportBudget {
11+
input_bytes: usize,
12+
advisories: usize,
13+
feedback_bytes: usize,
14+
}
15+
impl GradleOwaspReportBudget {
16+
/// 返回剩余输入字节预算,调用方仍须限制单份报告为8MiB。
17+
pub(crate) fn remaining_input(&self) -> usize {
18+
MAX_INPUT_BYTES - self.input_bytes
19+
}
20+
/// 登记实际读取字节与原生漏洞数;溢出时拒绝整轮观察,不静默截断。
21+
pub(crate) fn reserve_input(
22+
&mut self,
23+
bytes: usize,
24+
advisories: usize,
25+
) -> Result<(), &'static str> {
26+
let bytes = self
27+
.input_bytes
28+
.checked_add(bytes)
29+
.filter(|value| *value <= MAX_INPUT_BYTES)
30+
.ok_or(EXCEEDED)?;
31+
let advisories = self
32+
.advisories
33+
.checked_add(advisories)
34+
.filter(|value| *value <= MAX_ADVISORIES)
35+
.ok_or(EXCEEDED)?;
36+
self.input_bytes = bytes;
37+
self.advisories = advisories;
38+
Ok(())
39+
}
40+
/// 在保留单项反馈前登记序列化字节,计入数组分隔符的保守开销。
41+
pub(crate) fn reserve_feedback(&mut self, value: &Value) -> Result<(), &'static str> {
42+
let length = serde_json::to_vec(value).map_err(|_| EXCEEDED)?.len();
43+
self.feedback_bytes = self
44+
.feedback_bytes
45+
.checked_add(length)
46+
.and_then(|value| value.checked_add(2))
47+
.filter(|value| *value <= MAX_FEEDBACK_BYTES)
48+
.ok_or(EXCEEDED)?;
49+
Ok(())
50+
}
51+
}
52+
#[cfg(test)]
53+
mod tests {
54+
use super::{GradleOwaspReportBudget, MAX_INPUT_BYTES, MAX_ADVISORIES};
55+
#[test]
56+
fn exact_input_boundary_and_overflow_do_not_wrap_or_erase_budget() {
57+
let mut budget = GradleOwaspReportBudget::default();
58+
budget
59+
.reserve_input(MAX_INPUT_BYTES, MAX_ADVISORIES)
60+
.unwrap();
61+
assert_eq!(budget.remaining_input(), 0);
62+
assert!(budget.reserve_input(1, 0).is_err());
63+
assert!(budget.reserve_input(0, 1).is_err());
64+
assert!(budget.reserve_input(usize::MAX, 0).is_err());
65+
assert_eq!(budget.remaining_input(), 0);
66+
}
67+
}

‎crates/codeguard-cli/src/java_gradle_cve_command.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ pub fn run(args: &[String]) -> ExitCode {
6464
task_paths: options.tasks.clone(),
6565
};
6666
let native = observe(&request, &AtomicBool::new(false));
67-
let next_action = if native["native_status"] == "reports_observed_unverified" {
67+
let next_action = if native["reason"] == "gradle_owasp_report_budget_exceeded" {
68+
"累计报告或反馈超出预算;保留完整任务清单,将原任务分批执行并汇总所有分批结果。不得删除检查义务、降低规则或把未读取报告当作无漏洞;超大单任务需要独立的大报告处理方案,当前交付仍未评估"
69+
} else if native["native_status"] == "reports_observed_unverified" {
6870
"按原任务报告调查活动与原生抑制的漏洞,核对真实依赖归属和漏洞库时效;使用相同工具、输入、原任务复检。空报告不能证明无漏洞,当前持久任务与可信关闭未接线"
6971
} else {
7072
"读取原生reason恢复已有Gradle/JDK、原OWASP任务、原JSON配置和选定输入;缺插件或漏洞库属于环境阻塞,不修改无关源码。恢复后使用相同原任务复检"
7173
};
72-
let report = json!({"schema_version":"0.1.0","report_type":"java_gradle_cve_feedback","operation":"cve","language":"java","mode":"gradle","selected_inputs":options.files,"requested_task_paths":options.tasks,"budget":budget_record(timeout,source),"native":native,"next_action":next_action,"task_sync":"not_integrated","authority":"local_unverified","delivery_decision":"not_evaluated"});
74+
let report = json!({"schema_version":"0.2.0","report_type":"java_gradle_cve_feedback","operation":"cve","language":"java","mode":"gradle","selected_inputs":options.files,"requested_task_paths":options.tasks,"budget":budget_record(timeout,source),"native":native,"next_action":next_action,"task_sync":"not_integrated","authority":"local_unverified","delivery_decision":"not_evaluated"});
7375
if options.json {
7476
println!("{report}");
7577
} else {

‎crates/codeguard-cli/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,6 @@ pub mod java_gradle_cve_command;
523523

524524
#[cfg(unix)]
525525
mod gradle_module_cache;
526+
527+
#[cfg(unix)]
528+
mod gradle_owasp_report_budget;

‎crates/codeguard-cli/tests/gradle_dependency_check_probe.rs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,93 @@ fn public_sigint_returns_cancelled_feedback_without_quality_acceptance() {
535535
fs::write(path, serde_json::to_vec_pretty(&feedback).unwrap()).unwrap();
536536
}
537537
}
538+
539+
fn multi_report_fixture(native_reports: &[Value]) -> (Fixture, Request) {
540+
let mut native_model = model();
541+
native_model["projects"][0]["tasks"] = json!([]);
542+
let mut owner = ownership();
543+
owner["tasks"] = json!([]);
544+
let mut writes = String::new();
545+
let mut tasks = Vec::new();
546+
for (index, native_report) in native_reports.iter().enumerate() {
547+
let task = format!(":scan{index}");
548+
let path = format!("build/scan{index}/dependency-check-report.json");
549+
native_model["projects"][0]["tasks"].as_array_mut().unwrap().push(json!({"name":format!("scan{index}"),"implementation":"org.owasp.dependencycheck.gradle.tasks.Analyze","enabled":true}));
550+
owner["tasks"].as_array_mut().unwrap().push(json!({"project_path":":","task_path":task,"implementation":"org.owasp.dependencycheck.gradle.tasks.Analyze","report_project_name":"root project 'sample'","report_path":path}));
551+
writes +=
552+
&format!("mkdir -p build/scan{index}\ncat > {path} <<'JSON'\n{native_report}\nJSON\n");
553+
tasks.push(task);
554+
}
555+
let fixture = Fixture::new(&script(
556+
&native_model,
557+
&owner,
558+
&report(),
559+
&(writes + "exit 0"),
560+
));
561+
let mut request = fixture.request();
562+
request.task_paths = tasks;
563+
(fixture, request)
564+
}
565+
566+
#[test]
567+
fn aggregate_report_bytes_and_advisories_cannot_escape_per_file_limits() {
568+
for (name, native_reports) in [
569+
("bytes", {
570+
let mut sample = report();
571+
sample["padding"] = "x".repeat(6 * 1024 * 1024).into();
572+
vec![sample; 3]
573+
}),
574+
("advisories", {
575+
let mut sample = report();
576+
sample["dependencies"][0]["suppressedVulnerabilities"] = json!([]);
577+
sample["dependencies"][0]["vulnerabilities"] = json!(
578+
(0..501)
579+
.map(|index| json!({"source":"NVD","name":format!("CVE-2020-{index:04}")}))
580+
.collect::<Vec<_>>()
581+
);
582+
vec![sample; 2]
583+
}),
584+
] {
585+
let (_fixture, request) = multi_report_fixture(&native_reports);
586+
let out = observe(&request, &AtomicBool::new(false));
587+
assert_eq!(out["native_status"], "incomplete", "{name}: {out}");
588+
assert_eq!(out["reason"], "gradle_owasp_report_budget_exceeded");
589+
assert!(out["reports"].as_array().unwrap().is_empty());
590+
assert_no_acceptance(&out);
591+
}
592+
}
593+
594+
#[test]
595+
fn repeated_package_ids_cannot_expand_feedback_beyond_aggregate_budget() {
596+
let mut sample = report();
597+
sample["dependencies"][0]["packages"] = json!(
598+
(0..32)
599+
.map(|index| json!({"id":format!("pkg:maven/{index}/{}@1","x".repeat(480))}))
600+
.collect::<Vec<_>>()
601+
);
602+
sample["dependencies"][0]["suppressedVulnerabilities"] = json!([]);
603+
sample["dependencies"][0]["vulnerabilities"] = json!(
604+
(0..200)
605+
.map(|index| json!({"source":"NVD","name":format!("CVE-2020-{index:04}")}))
606+
.collect::<Vec<_>>()
607+
);
608+
let (_fixture, request) = multi_report_fixture(&[sample]);
609+
let out = observe(&request, &AtomicBool::new(false));
610+
assert_eq!(out["reason"], "gradle_owasp_report_budget_exceeded");
611+
assert_no_acceptance(&out);
612+
}
613+
614+
#[test]
615+
fn below_budget_multiple_reports_keep_every_task_and_suppressed_observation() {
616+
let (_fixture, request) = multi_report_fixture(&[report(), report()]);
617+
let out = observe(&request, &AtomicBool::new(false));
618+
assert_eq!(out["native_status"], "reports_observed_unverified");
619+
let reports = out["reports"].as_array().unwrap();
620+
assert_eq!(reports.len(), 2);
621+
for (index, row) in reports.iter().enumerate() {
622+
assert_eq!(row["task_path"], format!(":scan{index}"));
623+
assert_eq!(row["advisories"].as_array().unwrap().len(), 2);
624+
assert_eq!(row["advisories"][1]["suppressed_by_native_tool"], true);
625+
}
626+
assert_no_acceptance(&out);
627+
}

‎docs/Codeguard-Gradle-Vulnerability-Checks.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Only supported module/resource/metadata subtrees under an explicitly selected ex
3838

3939
## Dialogue report
4040

41-
`java_gradle_cve_feedback`0.1 wraps `gradle_dependency_check_probe`0.1 with original task requests, selected inputs, budget, native status/blockers, report digests and per-task advisories. Active and native-suppressed advisories are retained. Suppression does not mean resolution; package identifiers are not automatically attributed to the actual dependency graph.
41+
`java_gradle_cve_feedback`0.2 wraps `gradle_dependency_check_probe`0.2 with original task requests, selected inputs, budget, native status/blockers, report digests and per-task advisories. Active and native-suppressed advisories are retained. Suppression does not mean resolution; package identifiers are not automatically attributed to the actual dependency graph.
4242

4343
```json
4444
{
@@ -62,3 +62,5 @@ Controlled tests cover active/suppressed advisories, empty reports, exits0/1, co
6262
See [acceptance](../tests/acceptance/gradle-dependency-check-probe.md). `introduce-rust-codeguard-cli` remains the sole specification source; parent tasks stay open.
6363

6464
Native interface sources: [v12.1.0 AbstractAnalyze](https://github.com/dependency-check/dependency-check-gradle/blob/v12.1.0/src/main/groovy/org/owasp/dependencycheck/gradle/tasks/AbstractAnalyze.groovy), [ConfiguredTask](https://github.com/dependency-check/dependency-check-gradle/blob/v12.1.0/src/main/groovy/org/owasp/dependencycheck/gradle/tasks/ConfiguredTask.groovy).
65+
66+
Aggregate limits are16MiB report input,1000 advisory observations and2MiB serialized report feedback. Cancellation/deadlines are checked between reads and feedback records, including repeated package expansion. Exhaustion stays incomplete; no silent truncation or vulnerability-free verdict. Preserve all obligations when batching original tasks; oversized single tasks need a separate large-report design. Historical0.1 protocols/evidence stay unchanged;0.2 rejects downgrades. See [aggregate budget acceptance](../tests/acceptance/gradle-owasp-aggregate-budget.md).

‎docs/Codeguard-Gradle-Vulnerability-Checks.zh_CN.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ flowchart TD
4040

4141
## 对话报告
4242

43-
公开协议 `java_gradle_cve_feedback` 0.1包裹独立 `gradle_dependency_check_probe` 0.1。报告包括原请求任务、选定输入、预算来源、原生状态及阻塞、逐任务报告摘要和漏洞观察。活动及原生抑制项都保留;不把抑制等同于漏洞消失,不将包标识直接认定为项目真实依赖。
43+
公开协议 `java_gradle_cve_feedback` 0.2包裹独立 `gradle_dependency_check_probe` 0.2。报告包括原请求任务、选定输入、预算来源、原生状态及阻塞、逐任务报告摘要和漏洞观察。活动及原生抑制项都保留;不把抑制等同于漏洞消失,不将包标识直接认定为项目真实依赖。
4444

4545
```json
4646
{
@@ -66,3 +66,5 @@ flowchart TD
6666
详情:[局部验收](../tests/acceptance/gradle-dependency-check-probe.md)。唯一规格事实源仍为 `introduce-rust-codeguard-cli`,父任务不勾选。
6767

6868
原生接口依据:[v12.1.0 AbstractAnalyze](https://github.com/dependency-check/dependency-check-gradle/blob/v12.1.0/src/main/groovy/org/owasp/dependencycheck/gradle/tasks/AbstractAnalyze.groovy)、[ConfiguredTask](https://github.com/dependency-check/dependency-check-gradle/blob/v12.1.0/src/main/groovy/org/owasp/dependencycheck/gradle/tasks/ConfiguredTask.groovy)。
69+
70+
整轮累计报告输入上限16MiB、漏洞观察1000条、报告反馈2MiB;读报告及构造反馈时再次检查取消/截止时间。重复包标识放大也计入预算。超限为具体未完成,不静默截断或判为无漏洞;保留完整任务清单分批复检,超大单任务仍需独立大报告方案。历史0.1协议及证据保持原样,新0.2协议拒绝降级。见[累计预算验收](../tests/acceptance/gradle-owasp-aggregate-budget.md)。

‎openspec/changes/introduce-rust-codeguard-cli/specs/native-tool-adapters/spec.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,3 +943,7 @@ Model capture, original task execution and report ownership observation SHALL sh
943943
#### Scenario: An existing Gradle dependency cache enables offline original plugin resolution
944944
- **WHEN** --gradle-module-cache explicitly selects an existing caches/modules-2 directory
945945
- **THEN** Rust SHALL take a bounded immutable dependency-cache snapshot into the private Gradle user directory, reject user configuration and symlink escapes, preserve the cache digest and reject source-cache changes; it SHALL NOT modify or implicitly select the user's Gradle home or install plugin dependencies
946+
947+
#### Scenario: Individually bounded native reports exceed aggregate resource limits
948+
- **WHEN** multiple selected OWASP tasks exceed16MiB cumulative report input,1000 cumulative advisory observations or2MiB serialized report feedback, including repeated package identifiers expanding small inputs
949+
- **THEN** the application service SHALL stop bounded aggregation with a concrete incomplete budget observation, SHALL NOT silently truncate reports or issue an empty-clean verdict, and SHALL check cancellation/deadline between report reads and feedback records

0 commit comments

Comments
 (0)