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