| 1 | # Secure CI/CD Pipeline |
| 2 | |
| 3 | A GitHub Actions pipeline that gates every push and pull request on four security |
| 4 | checks before code is allowed to merge, then reports the run back to a SOC for |
| 5 | visibility. The sample app is a small Flask task API; the point of the repo is the |
| 6 | pipeline around it. |
| 7 | |
| 8 | The checks run as separate jobs so a failure tells you exactly which gate tripped |
| 9 | instead of one long log you have to scroll through. |
| 10 | |
| 11 | ```mermaid |
| 12 | flowchart TD |
| 13 | push[push / pull_request] --> lint[ruff lint] |
| 14 | lint --> sast[Semgrep SAST] |
| 15 | lint --> secrets[gitleaks secret scan] |
| 16 | lint --> deps[pip-audit dependencies] |
| 17 | sast --> test[pytest + coverage] |
| 18 | secrets --> test |
| 19 | deps --> test |
| 20 | test --> notify[notify SOC webhook] |
| 21 | ``` |
| 22 | |
| 23 | ## The gates |
| 24 | |
| 25 | | Job | Tool | What it stops | |
| 26 | |-----|------|---------------| |
| 27 | | `lint` | ruff | style + a security rule set (`S`) on top of the usual lint | |
| 28 | | `sast` | Semgrep | the OWASP/Flask rule packs plus four custom rules in `.semgrep/rules.yml` | |
| 29 | | `secrets` | gitleaks | committed credentials, full history scanned on PRs | |
| 30 | | `dependencies` | pip-audit | known-vulnerable pinned dependencies | |
| 31 | | `test` | pytest | regressions, with coverage reported | |
| 32 | | `notify-soc` | `scripts/notify_soc.py` | nothing - it posts the run outcome to the SOC | |
| 33 | |
| 34 | `lint` runs first as a cheap fail-fast. The three security scans fan out in |
| 35 | parallel, `test` waits on all of them, and the SOC notification runs last with |
| 36 | `if: always()` so the SOC hears about failures too, not just green runs. |
| 37 | |
| 38 | ## Custom Semgrep rules |
| 39 | |
| 40 | The packs catch the common cases; `.semgrep/rules.yml` adds the ones I kept seeing |
| 41 | slip through: |
| 42 | |
| 43 | - Flask started with `debug=True` (Werkzeug debugger RCE) |
| 44 | - `subprocess` with `shell=True` on a non-literal argument |
| 45 | - `jwt.decode` with signature verification disabled |
| 46 | - binding to `0.0.0.0` without an explicit reason |
| 47 | |
| 48 | The four rules running against a deliberately vulnerable file - every finding is |
| 49 | Blocking, so the SAST job fails the pipeline before the code can merge: |
| 50 | |
| 51 |  |
| 52 | |
| 53 | ## SARIF and the Security tab |
| 54 | |
| 55 | Semgrep emits SARIF that gets uploaded with `github/codeql-action/upload-sarif`, so |
| 56 | findings show up under the repo's Security tab and as inline PR annotations rather |
| 57 | than only in the job log. |
| 58 | |
| 59 | The dependency gate is `pip-audit`, which fails the build on a pinned package with a |
| 60 | known advisory: |
| 61 | |
| 62 |  |
| 63 | |
| 64 | ## Run it locally |
| 65 | |
| 66 | ```bash |
| 67 | make install |
| 68 | make all |
| 69 | ``` |
| 70 | |
| 71 | `make all` runs the same gates the pipeline does. You need `semgrep` and `gitleaks` |
| 72 | on your PATH for the `sast` and `secrets` targets; everything else is pip-installed |
| 73 | by `make install`. |
| 74 | |
| 75 | ## SOC integration |
| 76 | |
| 77 | The last job posts the run outcome - repo, commit, actor, status, and a link back to |
| 78 | the run - to a Shuffle webhook. In my lab that webhook feeds the |
| 79 | [SOC automation lab](../soc-automation-lab), so a failed security gate opens a case |
| 80 | in TheHive the same way a Wazuh alert does. Set the `SHUFFLE_WEBHOOK_URL` repository |
| 81 | secret to wire it up; without it the job no-ops instead of failing. |
| 82 | |
| 83 | More detail in [docs/pipeline.md](docs/pipeline.md) and |
| 84 | [docs/soc-integration.md](docs/soc-integration.md). |