Skip to main content
Heimdall findings represent discovered security vulnerabilities and logic flaws. Each finding contains rich metadata, remediation guidance, and a complete audit trail from discovery to resolution.

Finding Structure

Findings are stored in the findings table with comprehensive metadata:
schema
CREATE TABLE findings (
  id UUID PRIMARY KEY,
  scan_id UUID REFERENCES scans(id),
  repo_id UUID REFERENCES repos(id),
  
  -- Classification
  source VARCHAR(50) NOT NULL,           -- 'ai', 'semgrep', 'taint', 'config'
  severity VARCHAR(20) NOT NULL,         -- 'critical', 'high', 'medium', 'low'
  confidence VARCHAR(20) NOT NULL,       -- 'high', 'medium', 'low'
  status VARCHAR(20) DEFAULT 'open',     -- 'open', 'fixed', 'accepted', 'false_positive'
  
  -- Details
  title VARCHAR(500) NOT NULL,
  description TEXT,
  cwe_id VARCHAR(20),                    -- e.g., 'CWE-89'
  
  -- Location
  file_path VARCHAR(1000) NOT NULL,
  line_start INTEGER NOT NULL,
  line_end INTEGER,
  code_snippet TEXT,
  
  -- Evidence
  fingerprint VARCHAR(64) UNIQUE,        -- SHA256 hash for deduplication
  agent_reasoning TEXT,                  -- Step-by-step investigation notes
  
  -- Validation
  poc_validated BOOLEAN DEFAULT false,   -- Garmr confirmed exploitability
  poc_exploit_json JSONB,               -- PoC script and results
  
  -- Remediation
  suggested_patch TEXT,                  -- Unified diff format
  
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

Severity Levels

Heimdall uses a four-tier severity system:
Definition: Immediate threat to confidentiality, integrity, or availability. Exploitable without authentication.Examples:
  • SQL injection in public API
  • Authentication bypass
  • Remote code execution
  • Hardcoded admin credentials
  • Insecure deserialization
Response Time: Fix within 24 hoursRisk: Complete system compromise possible
Severity is initially assigned by the Hunt agent or static analyzer, then refined by Víðarr (adversarial verification) and Garmr (sandbox validation).

Confidence Levels

Confidence indicates certainty that the finding is a true positive:
ConfidenceDefinitionExample
HighValidated by Garmr PoC or obvious patternSQL injection confirmed with working exploit
MediumStrong evidence but not validatedAI agent traced input to SQL query without seeing parameterization
LowPotential issue requiring manual reviewStatic pattern matched but context unclear
Findings with poc_validated = true always have high confidence, regardless of the original assessment.

Finding Sources

The source field indicates which stage discovered the finding:
Stage: Hunt (agentic discovery)Characteristics:
  • Context-aware vulnerabilities
  • Logic flaws
  • Business logic issues
  • Complex data flow analysis
Confidence: Starts at medium, upgraded by Víðarr/Garmr
Stage: Static AnalysisCharacteristics:
  • Pattern-based detection
  • Known vulnerability signatures
  • Fast and deterministic
  • May have false positives
Confidence: Medium (requires manual review)
Stage: Taint AnalysisCharacteristics:
  • Data flow from source to sink
  • Input validation gaps
  • Sanitization failures
  • Cross-function taint propagation
Confidence: Medium to high (depends on flow confidence)
Stage: Config ScanCharacteristics:
  • IaC misconfigurations
  • Weak security settings
  • Missing encryption
  • Overly permissive policies
Confidence: High (objective checks)

Viewing Findings

Access findings through the API:
curl https://app.heimdall.security/api/v1/scans/{scan_id}/findings \
  -H "Authorization: Bearer YOUR_TOKEN"

Applying Patches

Some findings include AI-generated patches in unified diff format:
SELECT suggested_patch
FROM findings
WHERE id = 'YOUR_FINDING_ID';
Example patch:
--- src/api/search.rs
+++ src/api/search.rs
@@ -45,7 +45,8 @@
 async fn search_users(query: Query<SearchParams>) -> Result<Json<Vec<User>>> {
-    let sql = format!("SELECT * FROM users WHERE username LIKE '%{}%'", query.q);
-    let users = sqlx::query_as::<_, User>(&sql)
+    // Use parameterized query to prevent SQL injection
+    let users = sqlx::query_as::<_, User>(
+        "SELECT * FROM users WHERE username LIKE $1")
+        .bind(format!("%{}%", query.q))
         .fetch_all(&state.db)
         .await?;
     Ok(Json(users))

Apply via API

curl -X POST https://app.heimdall.security/api/v1/findings/{finding_id}/apply-patch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "commit_message": "fix: prevent SQL injection in user search",
    "create_branch": true,
    "branch_name": "heimdall/fix-sql-injection-search"
  }'
Always review AI-generated patches before merging. They may not account for all edge cases or architectural constraints.

Manual Application

Download the patch and apply locally:
# Download patch
curl https://app.heimdall.security/api/v1/findings/{finding_id}/patch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -o fix.patch

# Review
cat fix.patch

# Apply
git apply fix.patch

# Test
cargo test

# Commit
git add .
git commit -m "fix: apply Heimdall patch for SQL injection"

Creating Issues

Heimdall can automatically create issues in your repository:

Manual Issue Creation

curl -X POST https://app.heimdall.security/api/v1/findings/{finding_id}/create-issue \
  -H "Authorization: Bearer YOUR_TOKEN"
Generated issue format:
# [Heimdall] SQL injection in user search endpoint

**Severity:** Critical  
**Confidence:** High (validated)  
**CWE:** CWE-89 (SQL Injection)

## Description

The `search_users` function in `src/api/search.rs:45` constructs SQL queries using string formatting with unsanitized user input from the `q` query parameter. An attacker can inject arbitrary SQL commands to read, modify, or delete database contents.

## Location

- **File:** `src/api/search.rs`
- **Line:** 45-48
- **Endpoint:** `GET /api/users/search?q=<input>`

## Vulnerability Details

```rust
let sql = format!("SELECT * FROM users WHERE username LIKE '%{}%'", query.q);
let users = sqlx::query_as::<_, User>(&sql)
    .fetch_all(&state.db)
    .await?;

Proof of Concept

import requests

# Exfiltrate all users
payload = "' OR '1'='1"
response = requests.get(f"https://api.example.com/users/search?q={payload}")
print(response.json())  # Returns all users

# Drop users table
payload = "'; DROP TABLE users; --"
requests.get(f"https://api.example.com/users/search?q={payload}")
Use parameterized queries:
let users = sqlx::query_as::<_, User>(
    "SELECT * FROM users WHERE username LIKE $1")
    .bind(format!("%{}%", query.q))
    .fetch_all(&state.db)
    .await?;

References


This issue was automatically created by Heimdall Security

### Automatic Issue Creation

Enable auto-creation for high-severity findings:

```sql
UPDATE repos
SET 
  issue_auto_create_enabled = true,
  issue_auto_create_min_severity = 'high'  -- 'critical', 'high', 'medium', 'low'
WHERE id = 'YOUR_REPO_ID';
Configuration:
await heimdall.repos.updateIssueSettings(repoId, {
  autoCreateEnabled: true,
  minSeverity: 'high',
  requiresValidation: true  // Only create for poc_validated = true
});
Automatic creation happens during the Report stage:
src/pipeline/mod.rs
async fn auto_create_repo_issues(&self, repo: &Repo) {
    if !repo.issue_auto_create_enabled {
        return;
    }

    let qualifying_findings = findings
        .iter()
        .filter(|finding| {
            issues::finding_qualifies_for_auto_issue(
                finding,
                &repo.issue_auto_create_min_severity,
            )
        })
        .cloned()
        .collect::<Vec<Finding>>();

    for finding in qualifying_findings {
        match issues::create_or_link_issue(
            &self.db,
            self.encryption_key.as_ref(),
            repo,
            &finding,
            true, // auto_created = true
        ).await {
            Ok((repo_issue, was_created)) => {
                if was_created {
                    log::info!("Created issue: {}", repo_issue.issue_url);
                }
            }
            Err(e) => log::warn!("Issue creation failed: {e}"),
        }
    }
}

Finding Events

Every finding has a complete audit trail in the finding_events table:
schema
CREATE TABLE finding_events (
  id UUID PRIMARY KEY,
  finding_id UUID REFERENCES findings(id),
  user_id UUID REFERENCES users(id),
  event_type VARCHAR(50) NOT NULL,  -- 'status_changed', 'severity_changed', etc.
  old_value VARCHAR(500),
  new_value VARCHAR(500),
  comment TEXT,
  metadata JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);

Event Types

Event TypeDescriptionTriggered By
discoveredFinding created during scanHunt, Static Analysis, etc.
status_changedStatus updated (open → fixed)User action or patch application
severity_changedSeverity adjustedVíðarr or user override
validatedPoC confirmed exploitabilityGarmr
false_positive_markedMarked as false positiveVíðarr or user
issue_linkedGitHub/GitLab issue createdAuto-creation or manual
patch_appliedRemediation patch appliedAPI or git integration
comment_addedUser added noteUser annotation

Query Event History

SELECT 
  fe.event_type,
  fe.old_value,
  fe.new_value,
  fe.comment,
  fe.metadata,
  u.display_name as user,
  fe.created_at
FROM finding_events fe
LEFT JOIN users u ON u.id = fe.user_id
WHERE fe.finding_id = 'YOUR_FINDING_ID'
ORDER BY fe.created_at ASC;
Example timeline:
EventUserTimestampDetails
discoveredSystem2026-03-12 10:23:15Source: ai (Hunt agent)
validatedSystem2026-03-12 10:25:47Garmr confirmed with PoC
issue_linkedSystem2026-03-12 10:26:02Created GitHub issue #123
severity_changed[email protected]2026-03-12 14:30:00high → critical (exploited in wild)
patch_applied[email protected]2026-03-12 15:45:22Applied via PR #124
status_changed[email protected]2026-03-13 09:12:00open → fixed (merged and deployed)

Status Workflow

Findings progress through states:
Definition: Confirmed vulnerability requiring remediationActions:
  • Investigate
  • Apply patch
  • Create issue
  • Change severity

Update Status

curl -X PATCH https://app.heimdall.security/api/v1/findings/{finding_id} \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "fixed",
    "comment": "Deployed fix in release v2.1.4"
  }'

Fingerprinting & Deduplication

Each finding has a unique fingerprint based on:
fn make_fingerprint(title: &str, file: &str, line: i32) -> String {
    let input = format!("hunt:{title}:{file}:{line}");
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    hex::encode(hasher.finalize())
}
This prevents duplicate findings across scans:
CREATE UNIQUE INDEX findings_fingerprint_idx ON findings(fingerprint);
If the same vulnerability appears in multiple scans, Heimdall:
  1. Links to the original finding
  2. Updates last_seen_scan_id
  3. Creates a finding_recurred event
Fingerprints are line-number sensitive. Refactoring code may create “new” findings for the same vulnerability.

Next Steps

Hunt Agent

Learn how vulnerabilities are discovered

Sandbox Validation

Understand PoC validation with Garmr

Scan Pipeline

See the complete analysis workflow

Threat Modeling

Understand attack surface identification

Build docs developers (and LLMs) love