Demo · recorded 14 November 2026

Unvarnish on OWASP NodeGoat

NodeGoat is an OWASP training application that is insecure on purpose, so a review should find plenty. Below is a real run, command by command, with its output unedited apart from local paths, and the findings document it wrote.

The walkthrough

  1. Start an engagement

    An engagement is a folder that holds the tools under review and every artifact the run writes.

    $ unvarnish init nodegoat-review --client "Demo client" --consultant "Unvarnish demo"
    Created engagement at ./nodegoat-review
    Next: unvarnish add <name> --repo <url|path> from inside it.
  2. Let the dependency scanner look advisories up online

    osv-scanner runs offline by default, for air-gapped sites. For this public demo we let it query osv.dev, with one line in engagement.yaml:

    scanners:
      dependencies:
        offline: false
  3. Add the tool

    Unvarnish clones the repository with its full history, so the secrets scanner can search every commit and the git phase can measure who wrote what.

    $ unvarnish add nodegoat --repo https://github.com/OWASP/NodeGoat.git
    Added nodegoat (repo) at tools/nodegoat/source
      commit c5cb68a7084e on master
  4. Run it

    This run had no model configured, so the four model phases were skipped and the findings document comes from the deterministic scanners alone. The whole run took about twelve seconds.

    $ unvarnish run
    [11/14/26 10:11:23] INFO     split nodegoat: 106 units across 85 files (0 skipped)
    [11/14/26 10:11:24] INFO     render nodegoat: wrote findings.md
                                                run
    ┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
    ┃ tool     ┃ phase          ┃ status  ┃ detail                                            ┃
    ┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
    │ nodegoat │ inventory      │ ok      │ 111 files, 73 measured                            │
    │ nodegoat │ git            │ ok      │ 499 commits                                       │
    │ nodegoat │ scan           │ ok      │ 164 findings from 10 scanners                     │
    │ nodegoat │ split          │ ok      │ 106 units in 85 files, 0 skipped                  │
    │ nodegoat │ extract        │ skipped │ skipped: no model configured (set models.default) │
    │ nodegoat │ rollup_modules │ skipped │ skipped: no model configured (set models.default) │
    │ nodegoat │ rollup_tool    │ skipped │ skipped: no model configured (set models.default) │
    │ nodegoat │ verify         │ skipped │ skipped: no model configured (set models.default) │
    │ nodegoat │ render         │ ok      │ wrote findings.md                                 │
    └──────────┴────────────────┴─────────┴───────────────────────────────────────────────────┘
    Log: logs/run-20261114-101112.log
  5. Check where it stands

    Every phase records a hash of its inputs. A second run skips fresh phases and resumes where a stopped run left off.

    $ unvarnish status
                 status: Demo client
    ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
    ┃ tool         ┃ phase             ┃ state   ┃
    ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
    │ nodegoat     │ inventory         │ fresh   │
    │ nodegoat     │ git               │ fresh   │
    │ nodegoat     │ scan              │ fresh   │
    │ nodegoat     │ split             │ fresh   │
    │ nodegoat     │ extract           │ missing │
    │ nodegoat     │ rollup_modules    │ missing │
    │ nodegoat     │ rollup_tool       │ missing │
    │ nodegoat     │ verify            │ missing │
    │ nodegoat     │ render            │ 1 docs  │
    │ nodegoat     │ last run          │ clean   │
    ├──────────────┼───────────────────┼─────────┤
    │ (engagement) │ compare           │ missing │
    │ (engagement) │ render_comparison │ missing │
    └──────────────┴───────────────────┴─────────┘
  6. Read the findings

    The summary at the top of findings.md:

    ## Summary
    
    164 finding(s): 34 critical, 86 high, 37 medium, 7 low.
    
    **120 finding(s) are critical or high severity and need action before this tool is relied on further.**
    
    ## By the numbers
    
    | Measure | Value |
    |---|---|
    | Files analysed / total | 92 / 111 |
    | Source lines | 4,956 |
    | Largest source file | `app/views/tutorial/a1.html` (409 lines) |
    | Longest source function | `SessionHandler` in `app/routes/session.js` (268 lines) |
    | Languages (source lines) | html 2,933; javascript 2,005; dockerfile 18 |
    | Frameworks | Express |
    | Automated tests | present |
    | Secrets (current / history only) | 6 / 19 |
    | Client-exposed credentials / open tables | 0 / 0 |
    | Dependencies declared | 36 |
    | Dependency vulnerabilities | 311 (33 critical, 170 high, 94 medium, 14 low) |
    | End-of-life runtimes and frameworks | 1 past end of life, 0 near it; endoflife.date data dated 2026-11-14 |
    | Hardcoded infrastructure | 33 URLs · 0 internal host(s) · 1 IP(s) · 0 path(s) · 0 email(s) |
    | Commits · authors · active days | 499 · 40 · 204 |
    | History span | 2013-10-21 to 2023-06-21 |
    | Top author share · bus factor | 49% · 2 |
    | Commits over 2,000 lines | 7 |
    | AI tooling indicators | none found |

    Then the remediation outlook: every finding carries an effort band, and the bands are an input to your judgment, not a quote:

    ## Remediation outlook
    
    Findings by remediation effort: S up to half a day, M up to two days, L up to a week, XL weeks. Scanner findings carry a fixed band per rule; model findings carry the model's band, adjusted by the verifier where it checked the finding. The bands are an input to the consultant's judgment, not a quote.
    
    | Severity | S | M | L | XL | Not estimated | Total |
    |---|---:|---:|---:|---:|---:|---:|
    | critical | 34 | 0 | 0 | 0 | 0 | 34 |
    | high | 66 | 19 | 1 | 0 | 0 | 86 |
    | medium | 31 | 4 | 2 | 0 | 0 | 37 |
    | low | 7 | 0 | 0 | 0 | 0 | 7 |
    | **Total** | 138 | 23 | 3 | 0 | 0 | 164 |

    And the first critical finding. Secret values are always redacted and identified by fingerprint:

    #### Private key material
    
    `security` · source: scanner (secrets) · effort: S · id `7a8c5a682b3ae372`
    
    Private key material found in artifacts/cert/server.key. The value is redacted here and identified by fingerprint.
    
    **Impact.** Anyone who can read this code or its history holds a working credential. Rotation is required regardless of whether the file is still present.
    
    **Remediation.** Rotate the credential now. Move it to a secret manager or environment variable. Purge it from git history (git filter-repo) before the repository moves anywhere.
    
    **Effort.** S: rotate one credential and move it to configuration (scanner rule secrets.credential)
    
    **Evidence.**
    - `artifacts/cert/server.key:1` — `…[redacted 31 chars]` (fingerprint 71960cee9226; also appears in 1 commit(s) in git history)

    The full document lists all 164 findings with their evidence, then what was not analysed and why.

What this demo does not show

  • No model ran. With one configured, the same run adds model findings checked by the verifier, an executive summary, and the architecture and requirements documents.
  • One tool only, so there is no cross-tool comparison.
  • A first review, so there is no "Since the previous review" section; a follow-up run with --baseline adds one.
  • The run used gitleaks 8.30.1 and osv-scanner 2.7.1 from their release pages, and end-of-life dates fetched with unvarnish eol refresh before the run; without that data the end-of-life check is reported as not run. The Docker image bundles all three.
  • One medium finding is a heuristic: it lists route handlers with no authentication identifier nearby, and here it includes routes guarded by NodeGoat's isLoggedIn middleware. With a model configured, extraction is asked to confirm each one.
  • NodeGoat is meant to be insecure. These findings describe a training application, not a product anyone runs in production.