1use crate::core::ForgeGuardError;
4
5pub struct CiGenerator {
7 platform: String,
8}
9
10impl CiGenerator {
11 pub fn new(platform: &str) -> Self {
13 Self {
14 platform: platform.to_lowercase(),
15 }
16 }
17
18 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 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 pub fn generate_sbom_workflow(&self) -> String {
127 String::from(
128 r#"name: SBOM Generation
129
130on:
131 push:
132 branches: [ main, master ]
133 pull_request:
134 branches: [ main, master ]
135
136jobs:
137 sbom:
138 runs-on: ubuntu-latest
139 steps:
140 - uses: actions/checkout@v4
141 with:
142 submodules: recursive
143
144 - name: Install Forge Guard
145 run: |
146 cargo install forge-guard
147 forge-guard sbom --version
148
149 - name: Generate CycloneDX SBOM
150 run: forge-guard sbom --format cyclonedx --output sbom.cyclonedx.json
151
152 - name: Generate SPDX SBOM
153 run: forge-guard sbom --format spdx --output sbom.spdx.json
154
155 - name: Upload SBOM artifacts
156 uses: actions/upload-artifact@v4
157 with:
158 name: sbom
159 path: |
160 sbom.cyclonedx.json
161 sbom.spdx.json
162"#,
163 )
164 }
165
166 fn generate_gitlab(&self, include_deploy: bool) -> String {
167 let mut yaml = String::from(
168 r#"stages:
169 - security-audit
170 - fuzzing
171 - deploy
172
173variables:
174 FOUNDRY_PROFILE: ci
175
176cache:
177 key: ${CI_COMMIT_REF_SLUG}
178 paths:
179 - target/
180
181forge-guard:
182 stage: security-audit
183 image: ghcr.io/foundry-rs/foundry:latest
184 before_script:
185 - cargo install forge-guard || true
186 script:
187 - forge audit --strict
188 - forge scan --depth 1
189 artifacts:
190 paths:
191 - reports/
192 when: always
193
194fuzzing:
195 stage: fuzzing
196 image: ghcr.io/foundry-rs/foundry:latest
197 script:
198 - forge fuzz --runs 10000
199 - forge invariant --runs 1000
200"#,
201 );
202
203 if include_deploy {
204 yaml.push_str(
205 r#"
206deploy:
207 stage: deploy
208 image: ghcr.io/foundry-rs/foundry:latest
209 script:
210 - forge audit --strict --production
211 - forge deploy-safe
212 only:
213 - main
214 environment: production
215"#,
216 );
217 }
218
219 yaml
220 }
221
222 fn generate_bitbucket(&self, _include_deploy: bool) -> String {
223 String::from(
224 r#"image: ghcr.io/foundry-rs/foundry:latest
225
226pipelines:
227 default:
228 - step:
229 name: Security Audit
230 script:
231 - cargo install forge-guard || true
232 - forge audit --strict
233 - forge scan --depth 1
234 - forge fuzz --runs 10000
235 artifacts:
236 - reports/**
237
238 branches:
239 main:
240 - step:
241 name: Production Security Check
242 script:
243 - forge audit --strict --production
244 - forge deploy-safe
245 deployment: production
246"#,
247 )
248 }
249
250 fn generate_azure(&self, include_deploy: bool) -> String {
251 let mut yaml = String::from(
252 r#"trigger:
253 - main
254 - master
255
256pool:
257 vmImage: ubuntu-latest
258
259steps:
260 - checkout: self
261 submodules: recursive
262
263 - script: |
264 wget -q https://github.com/foundry-rs/foundry/releases/latest/download/foundry_linux_amd64.tar.gz
265 tar -xzf foundry_linux_amd64.tar.gz
266 export PATH=$PATH:$(pwd)
267 foundryup
268 displayName: 'Install Foundry'
269
270 - script: |
271 cargo install forge-guard
272 displayName: 'Install Forge Guard'
273
274 - script: |
275 forge audit --strict
276 displayName: 'Run Security Audit'
277
278 - script: |
279 forge fuzz --runs 10000
280 displayName: 'Run Fuzzing'
281
282 - script: |
283 forge invariant --runs 1000
284 displayName: 'Run Invariant Tests'
285
286 - script: |
287 forge scan --depth 1
288 displayName: 'Scan Dependencies'
289
290 - task: PublishBuildArtifacts@1
291 inputs:
292 pathToPublish: reports/
293 artifactName: 'audit-reports'
294"#,
295 );
296
297 if include_deploy {
298 yaml.push_str(
299 r#"
300 - script: |
301 forge audit --strict --production
302 forge deploy-safe
303 displayName: 'Safe Deploy' env:
304 ETH_RPC_URL: $(ETH_RPC_URL)
305"#,
306 );
307 }
308
309 yaml
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_ci_generator_github() {
319 let gen = CiGenerator::new("github");
320 assert_eq!(gen.filename(), "audit.yml");
321
322 let config = gen.generate(false).unwrap();
323 assert!(config.contains("name: Forge Guard Security Check"));
324 assert!(config.contains("forge audit --strict"));
325 assert!(config.contains("forge fuzz --runs 10000"));
326 }
327
328 #[test]
329 fn test_ci_generator_github_with_deploy() {
330 let gen = CiGenerator::new("github");
331 let config = gen.generate(true).unwrap();
332 assert!(config.contains("forge deploy-safe"));
333 assert!(config.contains("needs: [security-audit]"));
334 }
335
336 #[test]
337 fn test_ci_generator_gitlab() {
338 let gen = CiGenerator::new("gitlab");
339 assert_eq!(gen.filename(), ".gitlab-ci.yml");
340
341 let config = gen.generate(false).unwrap();
342 assert!(config.contains("forge-guard:"));
343 assert!(config.contains("forge audit --strict"));
344 }
345
346 #[test]
347 fn test_ci_generator_bitbucket() {
348 let gen = CiGenerator::new("bitbucket");
349 assert_eq!(gen.filename(), "bitbucket-pipelines.yml");
350
351 let config = gen.generate(false).unwrap();
352 assert!(config.contains("pipelines:"));
353 assert!(config.contains("forge audit --strict"));
354 }
355
356 #[test]
357 fn test_ci_generator_azure() {
358 let gen = CiGenerator::new("azure");
359 assert_eq!(gen.filename(), "azure-pipelines.yml");
360
361 let config = gen.generate(false).unwrap();
362 assert!(config.contains("vmImage: ubuntu-latest"));
363 assert!(config.contains("forge audit --strict"));
364 }
365
366 #[test]
367 fn test_ci_generator_invalid_platform() {
368 let gen = CiGenerator::new("invalid");
369 assert!(gen.generate(false).is_err());
370 }
371
372 #[test]
373 fn test_ci_generator_filenames() {
374 assert_eq!(CiGenerator::new("github").filename(), "audit.yml");
375 assert_eq!(CiGenerator::new("gitlab").filename(), ".gitlab-ci.yml");
376 assert_eq!(
377 CiGenerator::new("bitbucket").filename(),
378 "bitbucket-pipelines.yml"
379 );
380 assert_eq!(CiGenerator::new("azure").filename(), "azure-pipelines.yml");
381 assert_eq!(CiGenerator::new("unknown").filename(), "ci-config.yml");
382 }
383
384 #[test]
385 fn test_ci_generator_case_insensitivity() {
386 assert_eq!(CiGenerator::new("GitHub").filename(), "audit.yml");
387 assert_eq!(CiGenerator::new("GITLAB").filename(), ".gitlab-ci.yml");
388 assert!(CiGenerator::new("GitHub").generate(false).is_ok());
389 assert!(CiGenerator::new("GITLAB").generate(false).is_ok());
390 }
391
392 #[test]
393 fn test_ci_generator_bitbucket_with_deploy() {
394 let gen = CiGenerator::new("bitbucket");
395 let config = gen.generate(true).unwrap();
396 assert!(config.contains("forge deploy-safe"));
397 assert!(config.contains("forge audit --strict --production"));
398 assert!(config.contains("deployment: production"));
399 }
400
401 #[test]
402 fn test_ci_generator_azure_with_deploy() {
403 let gen = CiGenerator::new("azure");
404 let config = gen.generate(true).unwrap();
405 assert!(config.contains("forge deploy-safe"));
406 assert!(config.contains("forge audit --strict --production"));
407 assert!(config.contains("ETH_RPC_URL"));
408 }
409
410 #[test]
411 fn test_ci_generator_gitlab_without_deploy_no_deploy_section() {
412 let gen = CiGenerator::new("gitlab");
413 let config = gen.generate(false).unwrap();
414 assert!(
415 !config.contains("deploy:"),
416 "Should not contain deploy section"
417 );
418 assert!(
419 !config.contains("forge deploy-safe"),
420 "Should not contain deploy-safe"
421 );
422 }
423
424 #[test]
425 fn test_ci_generator_output_not_empty_for_all() {
426 let platforms = ["github", "gitlab", "bitbucket", "azure"];
427 for platform in platforms {
428 let gen = CiGenerator::new(platform);
429 let config = gen.generate(true).unwrap();
430 assert!(!config.is_empty(), "{} should produce output", platform);
431 }
432 }
433
434 #[test]
435 fn test_ci_generator_unsupported_error_message() {
436 let gen = CiGenerator::new("circle-ci");
437 let err = gen.generate(false).unwrap_err();
438 let msg = err.to_string();
439 assert!(msg.contains("Unsupported CI platform"));
440 assert!(msg.contains("circle-ci"));
441 assert!(msg.contains("github"));
442 assert!(msg.contains("gitlab"));
443 assert!(msg.contains("azure"));
444 }
445
446 #[test]
447 fn test_ci_generator_github_contains_all_sections() {
448 let gen = CiGenerator::new("github");
449 let config = gen.generate(true).unwrap();
450 assert!(config.contains("security-audit:"));
452 assert!(config.contains(" deploy:"));
454 assert!(config.contains("ETH_RPC_URL"));
456 assert!(config.contains("DEPLOYER_PRIVATE_KEY"));
457 }
458
459 #[test]
460 fn test_ci_generator_sbom_workflow() {
461 let gen = CiGenerator::new("github");
462 let wf = gen.generate_sbom_workflow();
463 assert!(wf.contains("name: SBOM Generation"));
464 assert!(wf.contains("forge-guard sbom --format cyclonedx --output sbom.cyclonedx.json"));
465 assert!(wf.contains("forge-guard sbom --format spdx --output sbom.spdx.json"));
466 assert!(wf.contains("actions/upload-artifact@v4"));
467 assert!(wf.contains("sbom.cyclonedx.json"));
468 }
469
470 #[test]
471 fn test_ci_generator_sbom_workflow_schedule_trigger() {
472 let gen = CiGenerator::new("github");
473 let wf = gen.generate_sbom_workflow();
474 assert!(wf.contains("branches: [ main, master ]"));
476 assert!(wf.contains("pull_request:"));
477 }
478
479 #[test]
480 fn test_ci_generator_platform_identity() {
481 let github = CiGenerator::new("github");
482 let gitlab = CiGenerator::new("gitlab");
483 let gh_config = github.generate(false).unwrap();
484 let gl_config = gitlab.generate(false).unwrap();
485 assert!(gh_config.contains("jobs:"));
487 assert!(gl_config.contains("stages:"));
488 assert!(gl_config.contains("forge-guard:"));
489 }
490}