Skip to main content

forge_guard/ci/
mod.rs

1//! CI/CD integration — generates pipeline configurations for various platforms.
2
3use crate::core::ForgeGuardError;
4
5/// Generator for CI/CD pipeline configurations.
6pub struct CiGenerator {
7    platform: String,
8}
9
10impl CiGenerator {
11    /// Create a new CI generator for the given platform.
12    pub fn new(platform: &str) -> Self {
13        Self {
14            platform: platform.to_lowercase(),
15        }
16    }
17
18    /// Get the output filename for this CI platform.
19    pub fn filename(&self) -> &'static str {
20        match self.platform.as_str() {
21            "github" => "audit.yml",
22            "gitlab" => ".gitlab-ci.yml",
23            "bitbucket" => "bitbucket-pipelines.yml",
24            "azure" => "azure-pipelines.yml",
25            _ => "ci-config.yml",
26        }
27    }
28
29    /// Generate the CI configuration content.
30    pub fn generate(&self, include_deploy: bool) -> Result<String, ForgeGuardError> {
31        match self.platform.as_str() {
32            "github" => Ok(self.generate_github(include_deploy)),
33            "gitlab" => Ok(self.generate_gitlab(include_deploy)),
34            "bitbucket" => Ok(self.generate_bitbucket(include_deploy)),
35            "azure" => Ok(self.generate_azure(include_deploy)),
36            _ => Err(ForgeGuardError::Config(format!(
37                "Unsupported CI platform: {}. Supported: github, gitlab, bitbucket, azure",
38                self.platform
39            ))),
40        }
41    }
42
43    fn generate_github(&self, include_deploy: bool) -> String {
44        let mut yaml = String::from(
45            r#"name: Forge Guard Security Check
46
47on:
48  push:
49    branches: [ main, master, develop ]
50  pull_request:
51    branches: [ main, master ]
52
53env:
54  FOUNDRY_PROFILE: ci
55
56jobs:
57  security-audit:
58    runs-on: ubuntu-latest
59    steps:
60      - uses: actions/checkout@v4
61        with:
62          submodules: recursive
63
64      - name: Install Foundry
65        uses: foundry-rs/foundry-toolchain@v1
66        with:
67          version: nightly
68
69      - name: Install Forge Guard
70        run: |
71          cargo install forge-guard
72          forge audit --version
73
74      - name: Run Security Audit
75        run: forge audit --strict
76
77      - name: Run Fuzzing Campaign
78        run: forge fuzz --runs 10000
79
80      - name: Run Invariant Tests
81        run: forge invariant --runs 1000
82
83      - name: Check Dependencies
84        run: forge scan --depth 1
85
86      - name: Generate Report
87        run: forge audit --report --markdown
88"#,
89        );
90
91        if include_deploy {
92            yaml.push_str(
93                r#"
94  deploy:
95    runs-on: ubuntu-latest
96    needs: [security-audit]
97    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
98    steps:
99      - uses: actions/checkout@v4
100        with:
101          submodules: recursive
102
103      - name: Install Foundry
104        uses: foundry-rs/foundry-toolchain@v1
105        with:
106          version: nightly
107
108      - name: Final Security Check
109        run: forge audit --strict --production
110
111      - name: Safe Deploy
112        run: forge deploy-safe
113        env:
114          ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
115          PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
116"#,
117            );
118        }
119
120        yaml
121    }
122
123    fn generate_gitlab(&self, include_deploy: bool) -> String {
124        let mut yaml = String::from(
125            r#"stages:
126  - security-audit
127  - fuzzing
128  - deploy
129
130variables:
131  FOUNDRY_PROFILE: ci
132
133cache:
134  key: ${CI_COMMIT_REF_SLUG}
135  paths:
136    - target/
137
138forge-guard:
139  stage: security-audit
140  image: ghcr.io/foundry-rs/foundry:latest
141  before_script:
142    - cargo install forge-guard || true
143  script:
144    - forge audit --strict
145    - forge scan --depth 1
146  artifacts:
147    paths:
148      - reports/
149    when: always
150
151fuzzing:
152  stage: fuzzing
153  image: ghcr.io/foundry-rs/foundry:latest
154  script:
155    - forge fuzz --runs 10000
156    - forge invariant --runs 1000
157"#,
158        );
159
160        if include_deploy {
161            yaml.push_str(
162                r#"
163deploy:
164  stage: deploy
165  image: ghcr.io/foundry-rs/foundry:latest
166  script:
167    - forge audit --strict --production
168    - forge deploy-safe
169  only:
170    - main
171  environment: production
172"#,
173            );
174        }
175
176        yaml
177    }
178
179    fn generate_bitbucket(&self, _include_deploy: bool) -> String {
180        String::from(
181            r#"image: ghcr.io/foundry-rs/foundry:latest
182
183pipelines:
184  default:
185    - step:
186        name: Security Audit
187        script:
188          - cargo install forge-guard || true
189          - forge audit --strict
190          - forge scan --depth 1
191          - forge fuzz --runs 10000
192        artifacts:
193          - reports/**
194
195  branches:
196    main:
197      - step:
198          name: Production Security Check
199          script:
200            - forge audit --strict --production
201            - forge deploy-safe
202          deployment: production
203"#,
204        )
205    }
206
207    fn generate_azure(&self, include_deploy: bool) -> String {
208        let mut yaml = String::from(
209            r#"trigger:
210  - main
211  - master
212
213pool:
214  vmImage: ubuntu-latest
215
216steps:
217  - checkout: self
218    submodules: recursive
219
220  - script: |
221      wget -q https://github.com/foundry-rs/foundry/releases/latest/download/foundry_linux_amd64.tar.gz
222      tar -xzf foundry_linux_amd64.tar.gz
223      export PATH=$PATH:$(pwd)
224      foundryup
225    displayName: 'Install Foundry'
226
227  - script: |
228      cargo install forge-guard
229    displayName: 'Install Forge Guard'
230
231  - script: |
232      forge audit --strict
233    displayName: 'Run Security Audit'
234
235  - script: |
236      forge fuzz --runs 10000
237    displayName: 'Run Fuzzing'
238
239  - script: |
240      forge invariant --runs 1000
241    displayName: 'Run Invariant Tests'
242
243  - script: |
244      forge scan --depth 1
245    displayName: 'Scan Dependencies'
246
247  - task: PublishBuildArtifacts@1
248    inputs:
249      pathToPublish: reports/
250      artifactName: 'audit-reports'
251"#,
252        );
253
254        if include_deploy {
255            yaml.push_str(
256                r#"
257  - script: |
258      forge audit --strict --production
259      forge deploy-safe
260    displayName: 'Safe Deploy'                env:
261      ETH_RPC_URL: $(ETH_RPC_URL)
262"#,
263            );
264        }
265
266        yaml
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_ci_generator_github() {
276        let gen = CiGenerator::new("github");
277        assert_eq!(gen.filename(), "audit.yml");
278
279        let config = gen.generate(false).unwrap();
280        assert!(config.contains("name: Forge Guard Security Check"));
281        assert!(config.contains("forge audit --strict"));
282        assert!(config.contains("forge fuzz --runs 10000"));
283    }
284
285    #[test]
286    fn test_ci_generator_github_with_deploy() {
287        let gen = CiGenerator::new("github");
288        let config = gen.generate(true).unwrap();
289        assert!(config.contains("forge deploy-safe"));
290        assert!(config.contains("needs: [security-audit]"));
291    }
292
293    #[test]
294    fn test_ci_generator_gitlab() {
295        let gen = CiGenerator::new("gitlab");
296        assert_eq!(gen.filename(), ".gitlab-ci.yml");
297
298        let config = gen.generate(false).unwrap();
299        assert!(config.contains("forge-guard:"));
300        assert!(config.contains("forge audit --strict"));
301    }
302
303    #[test]
304    fn test_ci_generator_bitbucket() {
305        let gen = CiGenerator::new("bitbucket");
306        assert_eq!(gen.filename(), "bitbucket-pipelines.yml");
307
308        let config = gen.generate(false).unwrap();
309        assert!(config.contains("pipelines:"));
310        assert!(config.contains("forge audit --strict"));
311    }
312
313    #[test]
314    fn test_ci_generator_azure() {
315        let gen = CiGenerator::new("azure");
316        assert_eq!(gen.filename(), "azure-pipelines.yml");
317
318        let config = gen.generate(false).unwrap();
319        assert!(config.contains("vmImage: ubuntu-latest"));
320        assert!(config.contains("forge audit --strict"));
321    }
322
323    #[test]
324    fn test_ci_generator_invalid_platform() {
325        let gen = CiGenerator::new("invalid");
326        assert!(gen.generate(false).is_err());
327    }
328
329    #[test]
330    fn test_ci_generator_filenames() {
331        assert_eq!(CiGenerator::new("github").filename(), "audit.yml");
332        assert_eq!(CiGenerator::new("gitlab").filename(), ".gitlab-ci.yml");
333        assert_eq!(
334            CiGenerator::new("bitbucket").filename(),
335            "bitbucket-pipelines.yml"
336        );
337        assert_eq!(CiGenerator::new("azure").filename(), "azure-pipelines.yml");
338        assert_eq!(CiGenerator::new("unknown").filename(), "ci-config.yml");
339    }
340}