100% Coverage. 0% Confidence.
Reverse-engineering a security scanner I built to actually fit real workflows.
This ran in under 1 minute (will depend on the vastness of your repo). No manual steps. No context-switching. Just a clean scan output telling me exactly what’s wrong with my Dockerfile: and how to fix it.
I started writing this tool a while ago. It started as a weekend project, something to scratch an itch I kept running into at work. Security scanners existed, sure. But they were either too slow, too noisy, too rigid, or too opinionated about how I should structure my repos. I wanted something simple that fit into my workflow, not the other way around.
So I built OpenGuardian.
Let’s reverse-engineer and dig deep to see how it works.
The Repo at a Glance
OpenGuardian is a static analysis tool written in Go. It scans Infrastructure as Code (Terraform, Ansible), containers (Docker, Kubernetes), and CI/CD pipelines for security misconfigurations. Think of it as a rules engine that reads your code, matches patterns, and tells you what’s broken before it hits production.
The structure is intentionally minimal:
├── cmd/ # CLI command definitions
├── config/ # Security rules (YAML)
├── pkg/ # Core scanning logic
├── samples/ # Intentionally bad code for testing
├── main.go # Entry pointWhen I was designing this, I kept one principle in mind: keep it simple and the surface area small. If someone clones the repo, they should be able to understand it in ten minutes. No hidden magic. No framework abstractions. Just folders that do what they say.
We’re going to trace one path through this: how a scan command becomes actionable output.
Starting at the End: The Scan Command
When you run this command: ./openguardian scan — dir ./samples/docker/bad, you’re invoking the scan command defined in cmd/. But the command itself doesn’t do the heavy lifting… it orchestrates.
Here’s what happens under the hood:
- Parse flags — directory, config path, severity filter, output format
- Load rules — read YAML files from `config/`
- Walk the directory — find files matching rule extensions
- Match patterns — apply regex rules to file contents
- Aggregate findings — group by file, sort by severity
- Render output — format as table, JSON, YAML, or CSV
Notice the command doesn’t contain the scanning logic. It delegates. This separation was deliberate. Early on, I realized I’d want to swap output formats without rewriting the scanner. I’d want to add new rules without touching Go code. Keeping these concerns apart meant the tool could evolve without breaking itself.
The Rules: Where Security Lives
Open config/docker_security_rules.yaml. This is where the actual security knowledge is encoded.
meta:
label: "Docker"
extensions: ["Dockerfile", "dockerfile"]
rules:
- id: "DOCKER_SENSITIVE_ENV"
name: "Hardcoded Secrets in ENV"
severity: "CRITICAL"
remediation: "Use build arguments or secret mounts."
patterns:
- 'ENV\s+\w*(PASSWORD|SECRET|KEY|TOKEN)\w*\s*='Let’s break this down.
- meta.extensions tells the scanner which files to check.
- id is a unique identifier, referenced in output and filters.
- severity ranges from CRITICAL to LOW.
- remediation is what shows up when you run, — verbose
- patterns is the regex that triggers the finding.
This is the design choice I’m most proud of: rules are data, not code.
Get mjmarc.life’s stories in your inbox
Join Medium for free to get updates from this writer.
You don’t need to recompile OpenGuardian to add a new security check. You write YAML. This means security teams can contribute rules without touching Go. It means you can version-control your ruleset separately from the tool itself. It means onboarding a new cloud provider is a config change, not a feature release.
When I first started, I had rules hardcoded in Go. It worked, but every time I wanted to add a check, I had to rebuild the binary. Moving to YAML was one of those “why didn’t I do this earlier” moments.
The tool ships with rulesets for AWS, Azure, Snowflake, Docker, Kubernetes, Ansible, GitHub Actions, Jenkins, and dbt. Each one follows the same structure.
The Config Behind the Config
There’s another layer: runtime configuration. This lives at ~/.openguardian/config.yaml and controls how the tool behaves, not what it checks.
version: 1
llm:
provider: ollama
model: llama3.1:8b
base_url: http://localhost:11434
temperature: 0.1
max_tokens: 2048
fix:
write_mode: inplace
max_files: 20
workers: 8
backup: false
report:
output_format: autoWait… LLM? Yes. OpenGuardian has a fix command that doesn’t just find issues; it generates remediation code. The LLM settings control which model handles that generation. I added this later, after spending too many hours manually fixing the same patterns over and over. If the tool knows what’s wrong, why not have it propose a fix?
But here’s the key separation: security rules and runtime behavior are decoupled. You can run the same ruleset with different concurrency settings, different output formats, different fix behaviors. The config/ folder is what to check. The runtime config is how to run.
This matters for teams. Your security ruleset can be centralized and standardized across the org. Your local runtime config can be personalized to your machine and preferences.
Verbose Mode: The Teaching Layer
Run the same scan with -v: ./openguardian scan — dir ./samples/docker/bad — severity CRITICAL -v
The tool shows you the exact line of code that triggered the rule and the remediation step. This is where OpenGuardian shifts from detection to education. It’s not just saying “you have a problem.” It’s saying “here’s the problem, here’s the code, here’s what to do instead.”
I built this because I got tired of scanners that handed me a list of rule IDs and expected me to go figure out what went wrong. Verbose mode was my answer to that frustration. For teams adopting security practices, this is the difference between a wall of errors and a guided path forward.
The Fix Command: Closing the Loop
Detection is step one. Remediation is step two. OpenGuardian’s fix command bridges them.
# Generate a fix plan (no changes written)
$ ./openguardian fix --dir ./samples --output ./fix-report.md
# Apply fixes after confirmation
$ ./openguardian fix --dir ./samples --applyWhen you run — apply, the tool runs a full scan, shows you what it plans to change, asks for confirmation (you have to type yes), then writes the changes in-place. If you’re paranoid, and in security, you should be, you can run without — apply first and review the plan.
The LLM integration means fixes aren’t just pattern replacements — they’re contextual. The model sees the surrounding code and generates appropriate remediation. It’s not perfect, but it’s a starting point that beats staring at a diff and guessing.
The Bigger Picture
We traced one path: Docker rules → scan command → output. But the same skeleton applies everywhere:
Technology | Rule File | Sample Checks
------------------|--------------------------------|------------------------------------------
AWS Terraform | aws_security_rules.yaml | Hardcoded keys, public RDS, open SGs
Azure Terraform | azure_security_rules.yaml | Public storage, open NSGs, disabled audit
Snowflake | snowflake_security_rules.yaml | Public shares, missing MFA, unmasked PII
Kubernetes | kubernetes_security_rules.yaml | Docker socket mounts, privileged containers
GitHub Actions | cicd_security_rules.yaml | Secrets in logs, root runners
dbt | dbt_security_rules.yaml | Missing owners, low test coverageSame structure. Same command. Different rules. That’s the whole point. Once you understand one path through the tool, you understand all of them.
The Handoff
You’ve seen how it works. Now make it yours.
# Clone
$ git clone https://github.com/marcelinojackson-org/DevSecOps.OpenGuardian.git
# Build
$ go build -o openguardian main.go
# Run against your own code
$ ./openguardian scan --dir /path/to/your/project -vTry these experiments:
- Add a custom rule: CREATE config/custom_security_rules.yaml, define one pattern, run a scan
- Break the samples: EDIT samples/docker/bad/Dockerfile, add a new violation, see if it catches it
- Change the output: RUN with — output json and pipe it somewhere useful
The point isn’t to use this tool exactly as I built it. It’s to understand the pattern: rules as data, config as behavior, output as education. Once you see it, you can apply it anywhere, or fork it and make something better.
That’s always been the goal.









