pub mod vscode;
use crate::core::ForgeGuardError;
pub struct CiGenerator {
platform: String,
}
impl CiGenerator {
pub fn new(platform: &str) -> Self {
Self {
platform: platform.to_lowercase(),
}
}
pub fn filename(&self) -> &'static str {
match self.platform.as_str() {
"github" => "audit.yml",
"gitlab" => ".gitlab-ci.yml",
"bitbucket" => "bitbucket-pipelines.yml",
"azure" => "azure-pipelines.yml",
_ => "ci-config.yml",
}
}
pub fn generate(&self, include_deploy: bool) -> Result<String, ForgeGuardError> {
match self.platform.as_str() {
"github" => Ok(self.generate_github(include_deploy)),
"gitlab" => Ok(self.generate_gitlab(include_deploy)),
"bitbucket" => Ok(self.generate_bitbucket(include_deploy)),
"azure" => Ok(self.generate_azure(include_deploy)),
_ => Err(ForgeGuardError::Config(format!(
"Unsupported CI platform: {}. Supported: github, gitlab, bitbucket, azure",
self.platform
))),
}
}
fn generate_github(&self, include_deploy: bool) -> String {
let mut yaml = String::from(
r#"name: Forge Guard Security Check
on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master ]
env:
FOUNDRY_PROFILE: ci
jobs:
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install Forge Guard
run: |
cargo install forge-guard
forge audit --version
- name: Run Security Audit
run: forge audit --strict
- name: Run Fuzzing Campaign
run: forge fuzz --runs 10000
- name: Run Invariant Tests
run: forge invariant --runs 1000
- name: Check Dependencies
run: forge scan --depth 1
- name: Generate Report
run: forge audit --report --markdown
"#,
);
if include_deploy {
yaml.push_str(
r#"
deploy:
runs-on: ubuntu-latest
needs: [security-audit]
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Final Security Check
run: forge audit --strict --production
- name: Safe Deploy
run: forge deploy-safe
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
"#,
);
}
yaml
}
pub fn generate_sbom_workflow(&self) -> String {
String::from(
r#"name: SBOM Generation
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Forge Guard
run: |
cargo install forge-guard
forge-guard sbom --version
- name: Generate CycloneDX SBOM
run: forge-guard sbom --format cyclonedx --output sbom.cyclonedx.json
- name: Generate SPDX SBOM
run: forge-guard sbom --format spdx --output sbom.spdx.json
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: sbom
path: |
sbom.cyclonedx.json
sbom.spdx.json
"#,
)
}
fn generate_gitlab(&self, include_deploy: bool) -> String {
let mut yaml = String::from(
r#"stages:
- security-audit
- fuzzing
- deploy
variables:
FOUNDRY_PROFILE: ci
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- target/
forge-guard:
stage: security-audit
image: ghcr.io/foundry-rs/foundry:latest
before_script:
- cargo install forge-guard || true
script:
- forge audit --strict
- forge scan --depth 1
artifacts:
paths:
- reports/
when: always
fuzzing:
stage: fuzzing
image: ghcr.io/foundry-rs/foundry:latest
script:
- forge fuzz --runs 10000
- forge invariant --runs 1000
"#,
);
if include_deploy {
yaml.push_str(
r#"
deploy:
stage: deploy
image: ghcr.io/foundry-rs/foundry:latest
script:
- forge audit --strict --production
- forge deploy-safe
only:
- main
environment: production
"#,
);
}
yaml
}
fn generate_bitbucket(&self, _include_deploy: bool) -> String {
String::from(
r#"image: ghcr.io/foundry-rs/foundry:latest
pipelines:
default:
- step:
name: Security Audit
script:
- cargo install forge-guard || true
- forge audit --strict
- forge scan --depth 1
- forge fuzz --runs 10000
artifacts:
- reports/**
branches:
main:
- step:
name: Production Security Check
script:
- forge audit --strict --production
- forge deploy-safe
deployment: production
"#,
)
}
fn generate_azure(&self, include_deploy: bool) -> String {
let mut yaml = String::from(
r#"trigger:
- main
- master
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
submodules: recursive
- script: |
wget -q https://github.com/foundry-rs/foundry/releases/latest/download/foundry_linux_amd64.tar.gz
tar -xzf foundry_linux_amd64.tar.gz
export PATH=$PATH:$(pwd)
foundryup
displayName: 'Install Foundry'
- script: |
cargo install forge-guard
displayName: 'Install Forge Guard'
- script: |
forge audit --strict
displayName: 'Run Security Audit'
- script: |
forge fuzz --runs 10000
displayName: 'Run Fuzzing'
- script: |
forge invariant --runs 1000
displayName: 'Run Invariant Tests'
- script: |
forge scan --depth 1
displayName: 'Scan Dependencies'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: reports/
artifactName: 'audit-reports'
"#,
);
if include_deploy {
yaml.push_str(
r#"
- script: |
forge audit --strict --production
forge deploy-safe
displayName: 'Safe Deploy' env:
ETH_RPC_URL: $(ETH_RPC_URL)
"#,
);
}
yaml
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ci_generator_github() {
let gen = CiGenerator::new("github");
assert_eq!(gen.filename(), "audit.yml");
let config = gen.generate(false).unwrap();
assert!(config.contains("name: Forge Guard Security Check"));
assert!(config.contains("forge audit --strict"));
assert!(config.contains("forge fuzz --runs 10000"));
}
#[test]
fn test_ci_generator_github_with_deploy() {
let gen = CiGenerator::new("github");
let config = gen.generate(true).unwrap();
assert!(config.contains("forge deploy-safe"));
assert!(config.contains("needs: [security-audit]"));
}
#[test]
fn test_ci_generator_gitlab() {
let gen = CiGenerator::new("gitlab");
assert_eq!(gen.filename(), ".gitlab-ci.yml");
let config = gen.generate(false).unwrap();
assert!(config.contains("forge-guard:"));
assert!(config.contains("forge audit --strict"));
}
#[test]
fn test_ci_generator_bitbucket() {
let gen = CiGenerator::new("bitbucket");
assert_eq!(gen.filename(), "bitbucket-pipelines.yml");
let config = gen.generate(false).unwrap();
assert!(config.contains("pipelines:"));
assert!(config.contains("forge audit --strict"));
}
#[test]
fn test_ci_generator_azure() {
let gen = CiGenerator::new("azure");
assert_eq!(gen.filename(), "azure-pipelines.yml");
let config = gen.generate(false).unwrap();
assert!(config.contains("vmImage: ubuntu-latest"));
assert!(config.contains("forge audit --strict"));
}
#[test]
fn test_ci_generator_invalid_platform() {
let gen = CiGenerator::new("invalid");
assert!(gen.generate(false).is_err());
}
#[test]
fn test_ci_generator_filenames() {
assert_eq!(CiGenerator::new("github").filename(), "audit.yml");
assert_eq!(CiGenerator::new("gitlab").filename(), ".gitlab-ci.yml");
assert_eq!(
CiGenerator::new("bitbucket").filename(),
"bitbucket-pipelines.yml"
);
assert_eq!(CiGenerator::new("azure").filename(), "azure-pipelines.yml");
assert_eq!(CiGenerator::new("unknown").filename(), "ci-config.yml");
}
#[test]
fn test_ci_generator_case_insensitivity() {
assert_eq!(CiGenerator::new("GitHub").filename(), "audit.yml");
assert_eq!(CiGenerator::new("GITLAB").filename(), ".gitlab-ci.yml");
assert!(CiGenerator::new("GitHub").generate(false).is_ok());
assert!(CiGenerator::new("GITLAB").generate(false).is_ok());
}
#[test]
fn test_ci_generator_bitbucket_with_deploy() {
let gen = CiGenerator::new("bitbucket");
let config = gen.generate(true).unwrap();
assert!(config.contains("forge deploy-safe"));
assert!(config.contains("forge audit --strict --production"));
assert!(config.contains("deployment: production"));
}
#[test]
fn test_ci_generator_azure_with_deploy() {
let gen = CiGenerator::new("azure");
let config = gen.generate(true).unwrap();
assert!(config.contains("forge deploy-safe"));
assert!(config.contains("forge audit --strict --production"));
assert!(config.contains("ETH_RPC_URL"));
}
#[test]
fn test_ci_generator_gitlab_without_deploy_no_deploy_section() {
let gen = CiGenerator::new("gitlab");
let config = gen.generate(false).unwrap();
assert!(
!config.contains("deploy:"),
"Should not contain deploy section"
);
assert!(
!config.contains("forge deploy-safe"),
"Should not contain deploy-safe"
);
}
#[test]
fn test_ci_generator_output_not_empty_for_all() {
let platforms = ["github", "gitlab", "bitbucket", "azure"];
for platform in platforms {
let gen = CiGenerator::new(platform);
let config = gen.generate(true).unwrap();
assert!(!config.is_empty(), "{} should produce output", platform);
}
}
#[test]
fn test_ci_generator_unsupported_error_message() {
let gen = CiGenerator::new("circle-ci");
let err = gen.generate(false).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Unsupported CI platform"));
assert!(msg.contains("circle-ci"));
assert!(msg.contains("github"));
assert!(msg.contains("gitlab"));
assert!(msg.contains("azure"));
}
#[test]
fn test_ci_generator_github_contains_all_sections() {
let gen = CiGenerator::new("github");
let config = gen.generate(true).unwrap();
assert!(config.contains("security-audit:"));
assert!(config.contains(" deploy:"));
assert!(config.contains("ETH_RPC_URL"));
assert!(config.contains("DEPLOYER_PRIVATE_KEY"));
}
#[test]
fn test_ci_generator_sbom_workflow() {
let gen = CiGenerator::new("github");
let wf = gen.generate_sbom_workflow();
assert!(wf.contains("name: SBOM Generation"));
assert!(wf.contains("forge-guard sbom --format cyclonedx --output sbom.cyclonedx.json"));
assert!(wf.contains("forge-guard sbom --format spdx --output sbom.spdx.json"));
assert!(wf.contains("actions/upload-artifact@v4"));
assert!(wf.contains("sbom.cyclonedx.json"));
}
#[test]
fn test_ci_generator_sbom_workflow_schedule_trigger() {
let gen = CiGenerator::new("github");
let wf = gen.generate_sbom_workflow();
assert!(wf.contains("branches: [ main, master ]"));
assert!(wf.contains("pull_request:"));
}
#[test]
fn test_ci_generator_platform_identity() {
let github = CiGenerator::new("github");
let gitlab = CiGenerator::new("gitlab");
let gh_config = github.generate(false).unwrap();
let gl_config = gitlab.generate(false).unwrap();
assert!(gh_config.contains("jobs:"));
assert!(gl_config.contains("stages:"));
assert!(gl_config.contains("forge-guard:"));
}
}