Sitemap

100% Coverage. 0% Confidence.

7 min readFeb 6, 2026

--

Press enter or click to view image in full size

Reverse-engineering a security scanner I built to actually fit real workflows.

Press enter or click to view image in full size
scan command and output

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 point

When 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.

Press enter or click to view image in full size
scan command and output

Here’s what happens under the hood:

  1. Parse flags — directory, config path, severity filter, output format
  2. Load rules — read YAML files from `config/`
  3. Walk the directory — find files matching rule extensions
  4. Match patterns — apply regex rules to file contents
  5. Aggregate findings — group by file, sort by severity
  6. 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.

Press enter or click to view image in full size
The Scan Pipeline

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.

Press enter or click to view image in full size
Anatomy of a Security Rule

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: auto

Wait… 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

Press enter or click to view image in full size
scan with filter and output

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

When 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.

Press enter or click to view image in full size
From Detection to Remediation

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 coverage

Same structure. Same command. Different rules. That’s the whole point. Once you understand one path through the tool, you understand all of them.

Press enter or click to view image in full size
What gitguardian covers

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

Try 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.

--

--

mjmarc.life
mjmarc.life

A curious and hyper-attentive person if the topic is right. A simple life is always good!