Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eljakani/ward/llms.txt

Use this file to discover all available pages before exploring further.

Pipeline Overview

Ward executes scans through a sequential 5-stage pipeline. Each stage has a specific responsibility and publishes events to track progress:
Provider  →  Resolvers  →  Scanners  →  Post-Process  →  Report
Implementation: internal/orchestrator/orchestrator.go:49-271

Stage 1: Provider

Purpose: Locate and prepare your project source code for scanning. The Provider stage determines where the project code comes from and ensures it’s accessible for analysis.

Provider Types

Source: internal/provider/local.goScans a project from the local filesystem.What it does:
  • Validates the path exists and is accessible
  • Checks for Laravel indicators (composer.json, artisan)
  • Returns the absolute path to the project root
Example:
ward scan /path/to/my-laravel-app
ward scan ./my-app

Laravel Detection

The provider checks if the project appears to be Laravel by looking for:
  • composer.json file
  • artisan CLI script
  • app/, config/, or routes/ directories
If these aren’t found, Ward emits a warning but continues scanning.

Events Published

EventStageStarted {
    Stage: StageProvider
}
The Provider automatically cleans up temporary directories (for Git clones) after the scan completes, even if there’s an error.

Stage 2: Resolvers

Purpose: Parse Laravel project files to build a structured ProjectContext that all scanners can consume. Resolvers run in priority order and populate different fields of the shared context.

Resolver Execution

Source: orchestrator.go:108-133
pc := &models.ProjectContext{}
resolvers := []resolver.ContextResolver{
    resolver.NewFrameworkResolver(),
    resolver.NewPackageResolver(),
}

for _, r := range resolvers {
    if err := r.Resolve(ctx, result.RootPath, pc); err != nil {
        // Log error but continue — partial context is still useful
    }
}

Resolver Types

Source: internal/resolver/framework.goPriority: 10 (runs first)What it parses:
  • composer.json → Laravel version, PHP version, project name
  • .env → Environment variables (APP_DEBUG, APP_ENV, DB_PASSWORD, etc.)
Context fields populated:
ProjectContext{
    LaravelVersion: "10.x",
    PHPVersion:     "8.2",
    ProjectName:   "my-app",
    FrameworkType: "laravel",
    ComposerDeps:  map[string]string{"laravel/framework": "^10.0"},
    EnvVariables:  map[string]string{"APP_DEBUG": "false"},
}

Project Context Structure

Source: internal/models/context.go
type ProjectContext struct {
    RootPath          string            // Absolute path to project root
    LaravelVersion    string            // e.g., "10.x"
    PHPVersion        string            // e.g., "8.2"
    ProjectName       string            // From composer.json name
    FrameworkType     string            // "laravel"
    ComposerDeps      map[string]string // composer.json require
    InstalledPackages map[string]string // composer.lock resolved versions
    EnvVariables      map[string]string // Parsed from .env
    ConfigFiles       []string          // List of config/*.php files
}

Events Published

EventContextResolved {
    ProjectName:    "my-app",
    LaravelVersion: "10.x",
    PHPVersion:     "8.2",
    FrameworkType:  "laravel",
    PackageCount:   87,
}
Resolvers are designed to fail gracefully. If a file is missing or malformed, the resolver logs an error but continues. Scanners that depend on that data will simply skip related checks.

Stage 3: Scanners

Purpose: Run independent security checks against the resolved project context. All scanners execute with the shared ProjectContext but operate independently. If one scanner fails, others continue.

Scanner Registration

Source: orchestrator.go:52-73
scanners := []models.Scanner{
    envscanner.New(),
    configscanner.New(),
    depscanner.New(),
}

// Load custom YAML rules
customRules, err := config.LoadAllRules(o.cfg)
if len(customRules) > 0 {
    scanners = append(scanners, rulesscanner.New(customRules))
}

// Filter disabled scanners
scanners = o.filterScanners(scanners)

Scanner Execution

Source: orchestrator.go:136-186 Each scanner:
  1. Publishes EventScannerStarted
  2. Receives the ProjectContext and an emit callback
  3. Performs its security checks
  4. Emits findings in real-time via callback
  5. Returns all findings as a slice
  6. Publishes EventScannerCompleted with finding count
emit := func(f models.Finding) {
    o.bus.Publish(eventbus.NewEvent(eventbus.EventFindingDiscovered, f))
}

findings, err := scanner.Scan(ctx, projectContext, emit)

Built-in Scanners

env-scanner

Source: internal/scanner/env/scanner.go8 checks for .env misconfigurations:
  • Missing .env file
  • APP_DEBUG=true
  • Empty/weak APP_KEY
  • Non-production APP_ENV
  • Empty DB_PASSWORD
  • File sessions in production
  • Real credentials in .env.example

config-scanner

Source: internal/scanner/configscan/scanner.go13 checks for config/*.php security issues:
  • Hardcoded debug mode
  • Weak encryption cipher
  • Missing cookie security flags
  • CORS wildcards
  • Hardcoded credentials in config
  • Long session/token lifetimes

dependency-scanner

Source: internal/scanner/dependency/scanner.goLive CVE database lookup via OSV.dev:
  • Queries for all packages in composer.lock
  • Batch requests for performance
  • Returns CVE ID, severity, affected versions
  • Includes remediation with fixed version

rules-scanner

Source: internal/scanner/rules/scanner.go40 default YAML pattern rules:
  • Secrets (7 rules)
  • Injection (6 rules)
  • XSS (4 rules)
  • Debug artifacts (6 rules)
  • Weak crypto (5 rules)
  • Security config (7 rules)
  • Auth issues (5 rules)

Scanner Interface

Source: internal/models/scanner.go All scanners implement:
type Scanner interface {
    Name() string
    Description() string
    Scan(ctx context.Context, project ProjectContext, emit func(Finding)) ([]Finding, error)
}
The emit callback allows scanners to stream findings in real-time. This enables the TUI to show live updates as vulnerabilities are discovered.

Scanner Configuration

You can disable scanners in ~/.ward/config.yaml:
scanners:
  disable:
    - dependency-scanner  # Skip CVE checks
    - rules-scanner       # Skip YAML rules

Events Published

EventScannerRegistered {
    Name:        "env-scanner",
    Description: "Environment file security checks",
}

Stage 4: Post-Process

Purpose: Clean, filter, and enrich scan results before reporting. The Post-Process stage applies several transformations to the raw findings:

Processing Steps

Source: orchestrator.go:188-204
1

Deduplication

Remove duplicate findings using a composite key:
key := finding.ID + "|" + finding.File + "|" + fmt.Sprintf("%d", finding.Line)
This ensures the same issue at the same location is only reported once, even if multiple scanners detect it.
2

Severity Filtering

Filter findings based on the minimum severity threshold from config:
severity: medium  # Only show medium, high, critical
Findings below the threshold are silently discarded.
3

Baseline Comparison

If a baseline file is provided (--baseline .ward-baseline.json), suppress known findings:
if o.baseline != nil {
    allFindings, suppressed = o.baseline.Filter(allFindings)
}
This shows only new findings introduced since the baseline was created.
4

History Diff

Compare with the last scan to show what changed:
diff, _ := store.CompareLast(report)
// "vs last scan: 2 new, 3 resolved (12→11)"
This helps track security posture over time.

Events Published

EventStageStarted { Stage: StagePostProcess }
EventStageCompleted { Stage: StagePostProcess }

Stage 5: Report

Purpose: Generate output files in configured formats and save scan history.

Report Generation

Source: orchestrator.go:206-250 Ward creates a ScanReport containing:
  • Project metadata from context
  • All findings (after post-processing)
  • Scan timestamps and duration
  • Scanner execution status
  • Any scanner errors
report := &models.ScanReport{
    ProjectContext: *pc,
    Findings:       allFindings,
    StartedAt:      startTime,
    CompletedAt:    endTime,
    Duration:       endTime.Sub(startTime),
    ScannersRun:    ["env-scanner", "config-scanner", ...],
    ScannerErrors:  map[string]string{},
}

Report Formats

Reporters are selected based on config.yaml:
output:
  formats:
    - json       # Always generated (baseline)
    - sarif      # GitHub Code Scanning
    - html       # Visual standalone report
    - markdown   # PR-friendly text format
  dir: ./reports
Source: internal/reporter/json.goMachine-readable format containing the full ScanReport structure.Output: ward-report.jsonAlways generated as a baseline, even if not in the formats list.

Scan History

Ward automatically saves each scan to ~/.ward/store/ for historical comparison: Source: internal/store/store.go
if _, err := store.Save(report); err != nil {
    // Log warning but don't fail the scan
}
Future scans of the same project will diff against this history.

Baseline Updates

If --update-baseline is provided, save current findings as the new baseline:
if o.baselinePath != "" {
    baseline.Save(o.baselinePath, allFindings)
}

Events Published

EventLogMessage {
    Level:   "info",
    Message: "Report written to ward-report.json",
}

Pipeline Error Handling

Ward uses graceful error handling throughout the pipeline:
Fatal — If the provider fails (path doesn’t exist, git clone fails), the scan stops immediately.
if err := src.Acquire(ctx, target); err != nil {
    return o.fail(fmt.Errorf("provider: %w", err))
}
Non-fatal — If a resolver fails (missing file, parse error), Ward logs an error but continues.Scanners that depend on that data will skip related checks.
Non-fatal — If a scanner fails, Ward records the error but continues with other scanners.The error is included in the final report’s ScannerErrors map.
Non-fatal — If a reporter fails, Ward logs an error but continues with other formats.At least JSON is always attempted.

Performance Characteristics

Parallel Scanning

Scanners could run in parallel (not currently implemented), as they’re fully independent and share an immutable context.

Shallow Clones

Git provider uses --depth=1 by default, significantly reducing clone time for large repositories.

Batch CVE Queries

Dependency scanner sends packages to OSV.dev in batches of 100 for optimal API performance.

Event Streaming

The emit callback allows real-time finding updates without waiting for scanner completion.

Architecture

System architecture and design principles

Security Scanners

Deep dive into each scanner’s checks