diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 778df922e8..317ad8b3b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,34 @@ jobs: toolchain: stable - name: Run performance benchmark run: './scripts/benchmark.sh --threshold 60 zsh rprompt' + dependency_review: + if: github.event_name == 'pull_request' + name: Dependency Review + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Dependency Review + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 + trivy: + name: Filesystem and Dependency Vulnerability Scan + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout Code + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + with: + persist-credentials: 'false' + - name: Filesystem and Dependency Vulnerability Scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 + with: + scan-type: fs + scanners: vuln + vuln-type: os,library + severity: HIGH,CRITICAL + ignore-unfixed: 'false' + exit-code: '1' draft_release: needs: - build diff --git a/crates/forge_ci/src/workflows/ci.rs b/crates/forge_ci/src/workflows/ci.rs index 2b5d81174f..8ec7b1c4af 100644 --- a/crates/forge_ci/src/workflows/ci.rs +++ b/crates/forge_ci/src/workflows/ci.rs @@ -30,6 +30,41 @@ pub fn generate_ci_workflow() { .run("./scripts/benchmark.sh --threshold 60 zsh rprompt"), ); + let dependency_review_job = Job::new("Dependency Review") + .cond(Expression::new("github.event_name == 'pull_request'")) + .permissions(Permissions::default().contents(Level::Read)) + .add_step(Step::new("Dependency Review").uses( + "actions", + "dependency-review-action", + "a1d282b36b6f3519aa1f3fc636f609c47dddb294", + )); + + let trivy_job = Job::new("Filesystem and Dependency Vulnerability Scan") + .permissions(Permissions::default().contents(Level::Read)) + .add_step( + Step::new("Checkout Code") + .uses( + "actions", + "checkout", + "d23441a48e516b6c34aea4fa41551a30e30af803", + ) + .with(("persist-credentials", "false")), + ) + .add_step( + Step::new("Filesystem and Dependency Vulnerability Scan") + .uses( + "aquasecurity", + "trivy-action", + "ed142fd0673e97e23eac54620cfb913e5ce36c25", + ) + .add_with(("scan-type", "fs")) + .add_with(("scanners", "vuln")) + .add_with(("vuln-type", "os,library")) + .add_with(("severity", "HIGH,CRITICAL")) + .add_with(("ignore-unfixed", "false")) + .add_with(("exit-code", "1")), + ); + let draft_release_job = jobs::create_draft_release_job("build"); let draft_release_pr_job = jobs::create_draft_release_pr_job(); let events = Event::default() @@ -73,6 +108,8 @@ pub fn generate_ci_workflow() { .add_env(("OPENROUTER_API_KEY", "${{secrets.OPENROUTER_API_KEY}}")) .add_job("build", build_job) .add_job("zsh_rprompt_perf", perf_test_job) + .add_job("dependency_review", dependency_review_job) + .add_job("trivy", trivy_job) .add_job("draft_release", draft_release_job) .add_job("draft_release_pr", draft_release_pr_job) .add_job("build_release", build_release_job) diff --git a/crates/forge_ci/tests/ci.rs b/crates/forge_ci/tests/ci.rs index 4856d76093..6446639403 100644 --- a/crates/forge_ci/tests/ci.rs +++ b/crates/forge_ci/tests/ci.rs @@ -5,6 +5,31 @@ fn generate() { workflow::generate_ci_workflow(); } +#[test] +fn generated_ci_preserves_blocking_pr_security_scans() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .expect("workspace root"); + let workflow = std::fs::read_to_string(root.join(".github/workflows/ci.yml")) + .expect("generated ci workflow is readable"); + + assert!(workflow.contains("dependency_review:")); + assert!(workflow.contains("Dependency Review")); + assert!(workflow + .contains("actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294")); + assert!(workflow.contains("if: github.event_name == 'pull_request'")); + assert!(workflow.contains("trivy:")); + assert!(workflow.contains("Filesystem and Dependency Vulnerability Scan")); + assert!(workflow.contains("aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25")); + assert!(workflow.contains("persist-credentials: 'false'")); + assert!(workflow.contains("scan-type: fs")); + assert!(workflow.contains("scanners: vuln")); + assert!(workflow.contains("exit-code: '1'")); + assert!(workflow.ends_with('\n')); + assert!(workflow.lines().all(|line| line.trim_end() == line)); +} + #[test] fn test_release_drafter() { workflow::generate_release_drafter_workflow();