Skip to main content

agentshield/ux/
ci.rs

1#[derive(Debug, Clone)]
2pub struct CiInstallOptions<'a> {
3    pub fail_on: &'a str,
4    pub ignore_tests: bool,
5    pub scan_path: &'a str,
6    pub baseline_path: Option<&'a str>,
7    pub upload_sarif: bool,
8}
9
10pub fn github_actions_workflow(options: &CiInstallOptions<'_>) -> String {
11    let baseline_input = options
12        .baseline_path
13        .map(|path| format!("          baseline: \"{path}\"\n"))
14        .unwrap_or_default();
15
16    format!(
17        r#"name: AgentShield
18
19on:
20  pull_request:
21  push:
22    branches: [main]
23
24permissions:
25  actions: read
26  contents: read
27  security-events: write
28
29jobs:
30  agentshield:
31    runs-on: ubuntu-latest
32    steps:
33      - uses: actions/checkout@v4
34      - uses: aiconnai/agentshield@main
35        with:
36          path: "{scan_path}"
37          fail-on: "{fail_on}"
38          ignore-tests: {ignore_tests}
39{baseline_input}          upload-sarif: {upload_sarif}
40          strict: true
41"#,
42        scan_path = options.scan_path,
43        fail_on = options.fail_on,
44        ignore_tests = options.ignore_tests,
45        baseline_input = baseline_input,
46        upload_sarif = options.upload_sarif,
47    )
48}
49
50pub fn github_actions_security_suite_workflow(options: &CiInstallOptions<'_>) -> String {
51    let baseline_input = options
52        .baseline_path
53        .map(|path| format!("          baseline: \"{path}\"\n"))
54        .unwrap_or_default();
55
56    format!(
57        r#"name: Security Suite
58
59on:
60  pull_request:
61  push:
62    branches: [main]
63  workflow_dispatch:
64
65permissions:
66  actions: read
67  contents: read
68  security-events: write
69
70jobs:
71  codeql:
72    name: CodeQL
73    runs-on: ubuntu-latest
74    steps:
75      - uses: actions/checkout@v6
76      - uses: github/codeql-action/init@v4
77        with:
78          queries: security-extended
79      - uses: github/codeql-action/analyze@v4
80
81  gitleaks:
82    name: Gitleaks
83    runs-on: ubuntu-latest
84    steps:
85      - uses: actions/checkout@v6
86        with:
87          fetch-depth: 0
88      - uses: gitleaks/gitleaks-action@v3
89        env:
90          GITHUB_TOKEN: ${{{{ secrets.GITHUB_TOKEN }}}}
91          # Required by Gitleaks for organization-owned repositories.
92          GITLEAKS_LICENSE: ${{{{ secrets.GITLEAKS_LICENSE }}}}
93
94  semgrep:
95    name: Semgrep CE
96    runs-on: ubuntu-latest
97    container:
98      image: semgrep/semgrep
99    if: github.actor != 'dependabot[bot]'
100    steps:
101      - uses: actions/checkout@v6
102      - run: semgrep scan --config auto
103
104  agentshield:
105    name: AgentShield
106    runs-on: ubuntu-latest
107    steps:
108      - uses: actions/checkout@v6
109      - uses: aiconnai/agentshield@main
110        with:
111          path: "{scan_path}"
112          fail-on: "{fail_on}"
113          ignore-tests: {ignore_tests}
114{baseline_input}          upload-sarif: {upload_sarif}
115          strict: true
116"#,
117        scan_path = options.scan_path,
118        fail_on = options.fail_on,
119        ignore_tests = options.ignore_tests,
120        baseline_input = baseline_input,
121        upload_sarif = options.upload_sarif,
122    )
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn ci_workflow_uses_expected_action_inputs() {
131        let workflow = github_actions_workflow(&CiInstallOptions {
132            fail_on: "high",
133            ignore_tests: true,
134            scan_path: ".",
135            baseline_path: None,
136            upload_sarif: true,
137        });
138
139        assert!(workflow.contains("uses: aiconnai/agentshield@main"));
140        assert!(workflow.contains("fail-on: \"high\""));
141        assert!(workflow.contains("ignore-tests: true"));
142        assert!(workflow.contains("upload-sarif: true"));
143        assert!(workflow.contains("strict: true"));
144        assert!(workflow.contains("actions: read"));
145        assert!(!workflow.contains("baseline:"));
146    }
147
148    #[test]
149    fn ci_workflow_can_use_baseline_file() {
150        let workflow = github_actions_workflow(&CiInstallOptions {
151            fail_on: "high",
152            ignore_tests: true,
153            scan_path: ".",
154            baseline_path: Some(".agentshield-baseline.json"),
155            upload_sarif: true,
156        });
157
158        assert!(workflow.contains("baseline: \".agentshield-baseline.json\""));
159        assert!(workflow.contains("upload-sarif: true"));
160        assert!(workflow.contains("strict: true"));
161        assert!(workflow.contains("actions: read"));
162    }
163
164    #[test]
165    fn ci_workflow_security_suite_includes_complementary_scanners() {
166        let workflow = github_actions_security_suite_workflow(&CiInstallOptions {
167            fail_on: "high",
168            ignore_tests: true,
169            scan_path: ".",
170            baseline_path: Some(".agentshield-baseline.json"),
171            upload_sarif: true,
172        });
173
174        assert!(workflow.contains("name: Security Suite"));
175        assert!(workflow.contains("uses: github/codeql-action/init@v4"));
176        assert!(workflow.contains("uses: gitleaks/gitleaks-action@v3"));
177        assert!(workflow.contains("semgrep scan --config auto"));
178        assert!(workflow.contains("uses: aiconnai/agentshield@main"));
179        assert!(workflow.contains("baseline: \".agentshield-baseline.json\""));
180        assert!(workflow.contains("strict: true"));
181        assert!(workflow.contains("actions: read"));
182    }
183}