MCPcopy Index your code
hub / github.com/Eljakani/ward

github.com/Eljakani/ward @v0.4.2

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.4.2 ↗ · + Follow
444 symbols 1,203 edges 66 files 186 documented · 42%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Ward

A security scanner built specifically for Laravel.

Ward understands your Laravel application — its routes, models, controllers, middleware, Blade templates, config files, .env secrets, Composer dependencies, and more. It doesn't just grep for patterns. It resolves your project's structure first, then runs targeted security checks against it.


Why Ward?

Laravel gives you a lot out of the box — CSRF protection, Eloquent's mass assignment guards, Bcrypt hashing, encrypted cookies. But it's easy to misconfigure things or leave gaps that standard linters won't catch:

  • APP_DEBUG=true shipping to production
  • A controller action with no authorization check
  • $guarded = [] on a model that handles payments
  • DB::raw() with interpolated user input
  • Session cookies without the Secure flag
  • An API route group missing auth:sanctum
  • Outdated Composer packages with known CVEs
  • Blade templates using {!! !!} on user data

Ward checks for all of these and more. It's designed to fit into the workflow you already have — run it locally during development, or wire it into CI to gate deployments.


How It Works

Ward scans your project in a pipeline of five stages:

 Provider  -->  Resolvers  -->  Scanners  -->  Post-Process  -->  Report

1. Provider — Locates and prepares your project source. Supports local paths and git URLs (shallow clone).

2. Resolvers — Parses composer.json, composer.lock, .env, and config/*.php to build a structured project context: framework version, PHP version, installed packages, environment variables, config files.

3. Scanners — Independent security checks run against the resolved context:

Scanner What it checks
env-scanner .env misconfigurations — debug mode, empty APP_KEY, non-production env, weak credentials, leaked secrets in .env.example
config-scanner config/*.php — hardcoded debug mode, session cookie flags, CORS wildcards, hardcoded credentials in config files
dependency-scanner composer.locklive CVE lookup via OSV.dev against the entire Packagist advisory database (no hardcoded list, always up-to-date)
rules-scanner 40 built-in YAML rules covering secrets, SQL/command/code injection, XSS, debug artifacts, weak crypto, auth issues, mass assignment, unsafe file uploads

4. Post-Process — Deduplicates findings, filters by minimum severity (from config), and diffs against your last scan to show what's new vs resolved.

5. Report — Generates output in multiple formats and saves scan history for trending.


Quick Start

Install

go install github.com/eljakani/ward@latest

Note: @latest resolves to the latest Git tag (e.g., v0.3.0). To install a specific version: go install github.com/eljakani/ward@v0.3.0

Make sure $GOPATH/bin is in your PATH (Go installs binaries there):

export PATH="$PATH:$(go env GOPATH)/bin"

Add this line to your ~/.bashrc or ~/.zshrc to make it permanent.

Or build from source:

git clone https://github.com/Eljakani/ward.git
cd ward
make build    # builds ./ward with embedded version, commit, and date
make install  # installs to $GOPATH/bin

Initialize

ward init

This creates ~/.ward/ with your configuration and 40 default security rules:

~/.ward/
├── config.yaml            # Main configuration
├── rules/                 # Security rules (YAML)
│   ├── secrets.yaml       # 7 rules: hardcoded passwords, API keys, AWS creds, JWT, tokens
│   ├── injection.yaml     # 6 rules: SQL injection, command injection, eval, unserialize
│   ├── xss.yaml           # 4 rules: unescaped Blade output, JS injection
│   ├── debug.yaml         # 6 rules: dd(), dump(), phpinfo(), debug bars
│   ├── crypto.yaml        # 5 rules: md5, sha1, rand(), mcrypt, base64-as-encryption
│   ├── security-config.yaml # 7 rules: CORS, SSL verify, CSRF, mass assignment, uploads
│   ├── auth.yaml          # 5 rules: missing middleware, rate limiting, loginUsingId
│   └── custom-example.yaml # Disabled template showing how to write your own rules
├── reports/               # Scan report output
└── store/                 # Scan history for diffing between runs

Scan a Local Project

ward scan /path/to/your/laravel-project

Scan a Remote Repository

ward scan https://github.com/user/laravel-project.git
```bash
ward scan ./my-app --output json

When no TTY is available or --output is specified, Ward runs in headless mode with styled text output — no interactive TUI.

CI Exit Codes (--fail-on)

# Exit code 1 if any High or Critical findings exist
ward scan . --output json --fail-on high

# Fail on any finding (including Info)
ward scan . --output json --fail-on info

Severity threshold is inclusive: --fail-on medium fails on Medium, High, and Critical.

Baseline (Suppress Known Findings)

On first run, generate a baseline of current findings:

ward scan . --output json --update-baseline .ward-baseline.json

On subsequent runs, suppress those known findings:

ward scan . --output json --baseline .ward-baseline.json --fail-on high

Only new findings (not in the baseline) will be reported. Commit .ward-baseline.json to your repo to track acknowledged findings.

CI Pipeline Example

- name: Run Ward
  run: |
    ward scan . --output json,sarif \
      --baseline .ward-baseline.json \
      --fail-on high

📖 Full CI/CD guide — GitHub Actions, GitLab CI, Bitbucket, Azure DevOps, Docker, caching, and troubleshooting: docs/ci-integration.md


Report Formats

Configure output formats in ~/.ward/config.yaml:

output:
  formats:
    - json       # ward-report.json  — machine-readable
    - sarif      # ward-report.sarif — GitHub Code Scanning / IDE integration
    - html       # ward-report.html  — standalone visual report (dark theme)
    - markdown   # ward-report.md    — text-based, great for PRs
  dir: ./reports

JSON is always generated as a baseline. All report files are written to the configured output directory (defaults to .).

GitHub Code Scanning Integration

Add the SARIF format and upload it in your CI workflow:

- name: Run Ward
  run: ward scan . --output json

- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: ward-report.sarif

See the GitHub Actions example below for a complete workflow.


Configuration

Ward loads its config from ~/.ward/config.yaml:

# Minimum severity to report: info, low, medium, high, critical
severity: info

output:
  formats: [json, sarif, html, markdown]
  dir: ./reports

scanners:
  disable: []     # scanner names to skip, e.g. ["dependency-scanner"]

rules:
  disable: []     # rule IDs to silence, e.g. ["DEBUG-001", "AUTH-001"]
  override:       # change severity for specific rules
    DEBUG-002:
      severity: low
  # custom_dirs:  # load rules from additional directories
  #   - /path/to/team-rules

providers:
  git_depth: 1    # shallow clone depth (0 = full history)

Custom Rules

Drop .yaml files into ~/.ward/rules/ and Ward picks them up automatically. See custom-example.yaml for a documented template.

rules:
  - id: TEAM-001
    title: "Hardcoded internal service URL"
    description: "Detects hardcoded URLs to internal services."
    severity: medium
    category: Configuration
    enabled: true
    patterns:
      - type: regex
        target: php-files
        pattern: 'https?://internal-service\.\w+'
    remediation: |
      Use environment variables:
        $url = env('INTERNAL_SERVICE_URL');

Pattern Types

Type Description
regex Regular expression match (line-by-line)
contains Exact substring match
file-exists Check if a file matching the glob exists
regex-scoped Regex match that suppresses findings inside a detected scope block

Targets

Target Files matched
php-files All .php files (recursive, skips vendor/)
blade-files resources/views/**/*.blade.php
config-files config/*.php
env-files .env, .env.*
routes-files routes/*.php
migration-files database/migrations/*.php
js-files resources/js/**/*.{js,ts,jsx,tsx}
path/to/*.ext Any custom glob pattern

Negative Patterns

Set negative: true to trigger when a pattern is absent — useful for "must have X" checks:

patterns:
  - type: contains
    target: blade-files
    pattern: "@csrf"
    negative: true    # fire if @csrf is NOT found

Scoped Patterns

Use type: regex-scoped when a match should only be flagged if it is not inside a surrounding block (e.g. a middleware group). The scanner reads the whole file, tracks brace depth to find blocks that start with scope_exclude, and suppresses any matches that fall within those blocks.

patterns:
  - type: regex-scoped
    target: routes-files
    pattern: 'Route::(get|post|put|patch|delete)\s*\([^;]*\)\s*;'
    scope_exclude: 'Route::middleware|->middleware\('
    exclude_pattern: '(login|register|password|reset|webhook|health)'
Field Required Description
pattern yes Regex to match on each line
scope_exclude yes Regex identifying lines that open a protected scope (brace-depth tracked)
exclude_pattern no Additional per-line exclusion applied after scope filtering (same as regex)
negative no Invert: fire when pattern is absent (same semantics as regex)

How it works:

  1. The file is scanned once to locate all scope blocks — lines matching scope_exclude that open a {...} block. The closing } is found by counting brace depth.
  2. Any line numbers that fall inside those ranges are marked as protected.
  3. The main pattern regex is then applied line-by-line, skipping protected lines.

Known limitations:

  • Brace characters inside string literals or comments can skew depth counting. Keep this in mind for files with unusual formatting.
  • Routes defined in separate files that are require'd inside a group are not linked across files; those will still be scanned in isolation.
  • For edge cases that slip through, use the baseline to permanently suppress confirmed false positives.

Rule Overrides

Disable or change severity of any rule in config.yaml without editing rule files:

rules:
  disable: [DEBUG-001, DEBUG-002]
  override:
    CRYPTO-003:
      severity: low
    AUTH-001:
      enabled: false

Scan History

Ward automatically saves each scan to ~/.ward/store/. On subsequent scans of the same project, it shows what changed:

  [info] vs last scan: 2 new, 3 resolved (12->11)

This lets you track security posture over time and catch regressions.


Terminal UI

Ward's TUI is built on Bubble Tea and adapts to both light and dark terminals automatically.

Scan View

Displayed while scanning is in progress — shows pipeline stage progress, scanner status with spinners, live severity counts, and a scrollable event log.

Results View

Displayed after scan completion — sortable findings table with severity badges, category grouping, and a detail panel showing description, code snippet, remediation, and references.

Keyboard Shortcuts

Key Action
q / Ctrl+C Quit
? Toggle help
Tab Switch view or panel
j / k / arrows Navigate findings
s Cycle sort column (severity, category, file)
Esc Back to scan view

CI Integration

GitHub Actions

Add this workflow to your Laravel project as .github/workflows/ward.yml:

```yaml name: Ward Security Scan on: [push, pull_request]

jobs: security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

  - name: Install Ward
    run: go install github.com/eljakani/ward@latest

  - name: Run Ward
    run: ward init && ward scan . --output json

  - name: Upload SARIF
    if: always()
    use

Extension points exported contracts — how you extend this code

Reporter (Interface)
Reporter generates output from a scan report. [4 implementers]
internal/reporter/reporter.go
Scanner (Interface)
Scanner is the interface that all security scanners implement. [4 implementers]
internal/models/scanner.go
ContextResolver (Interface)
ContextResolver builds a section of the ProjectContext. [2 implementers]
internal/resolver/resolver.go
SourceProvider (Interface)
SourceProvider abstracts where the project code comes from. [2 implementers]
internal/provider/provider.go
Handler (FuncType)
Handler is a function that processes an event.
internal/eventbus/bus.go

Core symbols most depended-on inside this repo

NewEvent
called by 33
internal/eventbus/events.go
Publish
called by 32
internal/eventbus/bus.go
Name
called by 26
internal/models/scanner.go
Scan
called by 19
internal/models/scanner.go
String
called by 19
internal/models/severity.go
esc
called by 15
internal/reporter/html.go
Close
called by 15
internal/eventbus/bus.go
findPattern
called by 14
internal/scanner/configscan/scanner.go

Shape

Function 203
Method 136
Struct 93
TypeAlias 7
Interface 4
FuncType 1

Languages

Go100%

Modules by API surface

internal/scanner/rules/scanner.go24 symbols
internal/reporter/sarif.go23 symbols
internal/scanner/dependency/scanner.go19 symbols
internal/eventbus/events.go18 symbols
internal/scanner/configscan/scanner.go17 symbols
internal/tui/views/results.go15 symbols
internal/tui/views/scan.go13 symbols
internal/tui/app.go13 symbols
internal/orchestrator/orchestrator.go13 symbols
internal/tui/components/findingdetail.go11 symbols
internal/store/store.go10 symbols
internal/scanner/env/scanner.go10 symbols

For agents

$ claude mcp add ward \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact