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 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}