Skip to content

Repository files navigation

Mantis - CI/CD Security Gate

CI Latest Release License Go Version Go Report Card Docker Image Deterministic Detection Destructive Testing: Opt-in PRs Welcome

Mantis

A template-driven DAST and application validation tool, built to run as a step in a deployment pipeline rather than as a standalone scanner someone kicks off by hand.

What problem it solves

After every deploy, there are really two questions worth asking: did the app actually come up working, and did this deploy introduce anything security-relevant. Most pipelines answer neither in a structured way - a smoke check here, a scanner report nobody opens there, no single gate that actually blocks a bad deploy.

Mantis answers both with one command and one exit code. Smoke tests answer "does it work." Templates, a crawler with passive checks, and OpenAPI-driven API tests answer "is it secure." Detection is deterministic on purpose - no model in the loop deciding whether something's a finding. A template either matches or it doesn't, so a given target and a given template set always produce the same result.

Features

Template engine

YAML templates in the Nuclei style: request chaining, five matcher types (status/word/regex/json/dsl), three extractor types (json/regex/header), and a small DSL for matcher/assertion expressions - parsed with Go's own go/parser instead of a third-party expression library. 22 starter templates ship in templates-community/ (see mantis templates list).

Payload fuzzing. A payloads block defines named value lists that get substituted into {{name}} placeholders in any request field. Three attack modes: sniper (one set, one value at a time), pitchfork (multiple sets zipped in lockstep), and clusterbomb (full Cartesian product of all sets). Every combination runs the full request chain independently.

Conditional steps. A when field on any request takes a DSL expression evaluated against variables extracted by earlier steps. A false result skips that request without failing the chain, so a template can branch on what it actually found rather than what it assumed.

Flow scripting. The optional flow field replaces the default sequential chain with a small scripting language: http(N) calls requests by index, if/else/end branches on conditions (including httpN_status, httpN_matched, httpN_body after each call), set name = expr stores a value for later steps and {{name}} rendering, and stop terminates early keeping any findings produced so far.

Global matchers. A passive layer runs against every HTTP response in every template execution - no extra requests. Eight built-in patterns catch secrets and noise that templates don't explicitly look for: AWS keys, private key material, JWTs, SQL error messages, stack traces, PHP errors, and ASP.NET errors. Findings from this layer are reported separately from template-match findings.

DAST

A crawler with inline passive checks (missing security headers, insecure cookies, CORS misconfiguration, server version disclosure, directory listing) running against every page it fetches, followed by active testing: the loaded template set against the target root, plus per-parameter fuzzing (SQL injection, XSS, path traversal, and a couple more classes) against every discovered query parameter - always GET-only and safe to retry. Fuzzing can optionally extend to form submissions too, but that's opt-in and explained in Environment policy and destructive testing below, not something that happens by default.

Smoke testing

YAML workflows that chain HTTP requests through extracted variables, with JSONPath and DSL assertions, best-effort cleanup requests, and dependencies between workflows.

API testing

Point it at an OpenAPI 3 (or Swagger 2) spec and it generates checks for missing authentication, undeclared HTTP method acceptance, broken object level authorization, and sensitive-looking keys in response bodies.

BOLA gets two modes depending on what auth is configured. With a single identity it's a heuristic: two different sample IDs both returning 200 under the same credentials, flagged at reduced confidence since it doesn't actually prove anything. With two or more identities configured in environments.yaml (each with a known owned resource id via owns), it's a real check: identity A requesting a resource identity B is known to own, and getting a 200 back, is a confirmed finding at full confidence - not a guess, because the test fixtures tell Mantis exactly who owns what.

Environment-aware policy

Environments resolve to a security level (aggressive/standard/passive) that controls how deep testing goes - crawl depth, rate limits, whether active testing runs at all. Any environment name that isn't found, or has no recognized security_level, resolves to passive, the safest policy - a typo should never grant more access than intended. Full details, including the double opt-in required for anything destructive, are in Environment policy and destructive testing.

Security gate

mantis validate runs smoke + passive DAST + active DAST + optional API tests according to the target environment's policy, then makes a single pass/fail decision with a real exit code. Before running any checks, every command does a baseline reachability request first - a target that's completely unreachable fails loudly with an operational error instead of silently running zero checks and reporting a clean pass.

Reporting

Console, JSON, SARIF 2.1.0, JUnit XML, HTML, a native Azure DevOps format that streams ##vso[...] logging commands directly to a pipeline's console (findings show up as real entries in the Issues panel, no marketplace extension needed), and a native GitHub Actions format that emits ::error/::warning/::notice annotations plus a markdown job summary. --report takes a comma-separated list (junit,sarif,html,azdo or junit,sarif,html,github) so one scan produces every output you need.

Architecture

cmd/mantis/          CLI entry point and command wiring
internal/
  httpclient/         the one HTTP execution path - scope, rate limits,
                       redirects, response size caps, secret redaction
  templates/           YAML parsing, matchers, extractors, request chaining
  dsl/                 the matcher/assertion expression language
  jsonpath/             minimal JSONPath used by matchers and extractors
  dast/                crawler + passive checks + active scan orchestration
  globalmatchers/       passive secret/error patterns evaluated on every response
  attacks/              per-parameter fuzzing (sqli/xss/path-traversal/ssti/cmdi)
  smoke/               smoke workflow parsing and execution
  api/                 OpenAPI reader + generated API security checks
  environments/        environment profiles and security-level policy
  gate/                 the pass/fail decision
  findings/             the shared finding type
  reporters/            console/json/sarif/junit/html/azdo/github output
templates-community/  starter security templates
smoke/                example smoke workflow
ci/                   example pipeline integrations (Azure Pipelines, GitHub Actions)

Two external dependencies (gopkg.in/yaml.v3, golang.org/x/net/html). Everything else - the HTTP client, the matcher/extractor engine, the DSL, the OpenAPI reader, SARIF/JUnit output - is hand-rolled to keep this light rather than dragging in a pile of transitive dependencies for what are, individually, fairly small problems.

Installation

Option 1: Download a release binary (recommended)

  1. Grab the archive for your platform from the latest release: mantis_<version>_linux_amd64.tar.gz (or _arm64), mantis_<version>_darwin_amd64.tar.gz (or _arm64 for Apple Silicon), or mantis_<version>_windows_amd64.zip (or _arm64).
  2. Extract it. Each archive contains the mantis binary, templates-community/, README.md and LICENSE:
    tar -xzf mantis_0.1.0_linux_amd64.tar.gz
    cd mantis_0.1.0_linux_amd64
  3. (Recommended) verify the download against checksums.txt from the same release before running it:
    sha256sum -c checksums.txt --ignore-missing
  4. Confirm it works:
    ./mantis version
    # mantis v0.1.0 (commit ..., built ...)
  5. Optionally put it on your PATH - but keep templates-community/ next to wherever you actually run mantis from. It's the default --templates-dir, and every template-driven command needs it unless you point --templates-dir somewhere else.

Option 2: Docker

docker pull ghcr.io/bogdanticu88/mantis:latest
docker run --rm ghcr.io/bogdanticu88/mantis:latest version

The image is built from a distroless base with templates-community/ baked in at /app, so the default relative paths work with no extra mounting - docker run --rm ghcr.io/bogdanticu88/mantis:latest scan <target> --fail-on high works immediately. For an environments file or custom templates, mount them in and point the flags at the mounted path:

docker run --rm -v "$(pwd)":/data ghcr.io/bogdanticu88/mantis:latest \
  validate --environment test --environments-file /data/environments.yaml

Option 3: Build from source

Requires Go 1.23+.

git clone https://github.com/bogdanticu88/Mantis.git
cd Mantis
go build -o mantis ./cmd/mantis
./mantis version

Getting started

A four-step walkthrough. Replace <target> throughout with your own application's URL, or a target you're explicitly authorized to test.

1. Run a template scan

mantis scan <target> --fail-on high

This loads every template under templates-community/ (22 ship by default - mantis templates list to see them) and reports anything that matches. Real output looks like this:

MANTIS Security Gate

Environment:
Target: <target>

Findings
✗ [HIGH] Spring Boot Actuator Exposure (GET /actuator/env)
✗ [MEDIUM] Cookie Set Without Secure/HttpOnly Flags (GET /)
✗ [LOW] HTTP TRACE Method Enabled (TRACE /)

CRITICAL: 0
HIGH: 1
MEDIUM: 1
LOW: 1
INFO: 0

Pipeline: FAILED
Exit code: 1

--fail-on high means anything HIGH or worse trips the gate (exit code 1) - that's what a pipeline step checks. Nothing found, or everything below the threshold, exits 0.

2. Run full DAST

mantis dast <target> --fail-on high

Same idea, but it crawls first (discovers pages, forms, query parameters), layers passive checks (missing headers, insecure cookies, CORS) on every page it fetches, then runs the templates plus query-parameter fuzzing (SQLi/XSS/path traversal and more) against everything it found - this invocation alone never submits any form or touches anything destructive; that needs an explicit opt-in, see Environment policy and destructive testing.

3. Write and run a smoke test

Create smoke/health.yaml:

id: health
type: smoke
steps:
  - id: check
    request:
      method: GET
      path: /
    assertions:
      - status: 200
mantis smoke --target <target> --workflows-dir smoke

This is the "does it actually work" half of Mantis, unrelated to security - it just confirms the deployment came up. See smoke/payments-lifecycle.yaml for a fuller example with request chaining, variable extraction, and cleanup.

4. Set up environments and run the real gate

Copy environments.example.yaml to environments.yaml and point dev/test/etc. at your real base URLs and security_level. Then:

mantis validate --environment test --fail-on high \
  --report junit,sarif,html --output-dir ./reports

This is what actually goes in CI: smoke + passive DAST + active DAST (plus API tests if you add --openapi), gated by the target environment's policy, collapsed into one pass/fail exit code. See ci/azure-pipelines.example.yml or ci/github-actions.example.yml for a complete pipeline step block, or --report github/--report azdo for native annotations in either platform.

Every scan/dast/smoke/api/validate command accepts --environment, --environments-file, --fail-on critical|high|medium|low|any, --report (comma-separated: json,sarif,junit,html,azdo,github), --output/--output-dir, --insecure-skip-verify, --timeout. Flags can go before or after the positional target.

Environment policy and destructive testing

Everything above - scan, dast, smoke, validate as shown in Getting started - only ever reads. It's safe to run against anything you're authorized to point it at. This section covers the one part of Mantis that isn't: destructive testing, which is opt-in, off by default, and worth understanding fully before you turn it on.

The policy table

Environments resolve to a security level that controls how deep testing goes:

Level Smoke Passive DAST Active DAST Destructive
aggressive Yes Yes Yes Yes*
standard Yes Yes Yes No
passive Yes Yes No No

* Authorized in principle, not run automatically - see below.

Any environment name that isn't found, or has no recognized security_level, resolves to passive, the safest policy. A typo in an environment name should never grant more access than intended.

What "Destructive" actually means

Two mechanisms, both of which send real, state-changing requests against your target:

  • API method-abuse probing (mantis api scan) tries HTTP methods your OpenAPI spec doesn't declare for a path - POST, PUT, PATCH, DELETE - to see if the server accepts them anyway.
  • Form fuzzing (mantis dast / mantis validate) submits any non-GET form the crawler finds, with an injection payload in one field at a time. That's a real POST hitting real business logic - if the target doesn't reject the malformed input, it creates an actual record (a comment, an order, whatever the form does) containing the payload. The payloads themselves are chosen to be low-impact (echo markers, arithmetic markers, not DROP TABLE), but creating a real record is still a real side effect, not a simulated one.

Query-parameter fuzzing (covered in Getting started, step 2) is unaffected by any of this - it's GET-only, always safe, and runs whenever active testing is on regardless of the destructive setting.

Two gates, not one

security_level: aggressive in environments.yaml authorizes destructive testing - it does not run it. mantis api scan, mantis dast, and mantis validate all additionally require an explicit --destructive flag on that specific invocation:

mantis validate --environment dev --destructive

Without --destructive, an aggressive environment still runs full crawl depth, every template, and query-parameter fuzzing - everything except the two destructive mechanisms above. One line in a config file is never enough, by itself, to make a future pipeline run start submitting forms; someone has to type the flag on the run that should.

If you want full crawl/template/rate-limit depth without ever allowing destructive testing on a given environment - even if --destructive gets added to a pipeline step by mistake later - set allow_destructive: false explicitly, which overrides the level's default:

environments:
  test:
    base_url: https://test.example.com
    security_level: aggressive
    allow_destructive: false

Only turn this on against environments where creating throwaway records is genuinely fine. Never against production. Think carefully before using it against a shared test environment other people's work depends on.

Development setup

make build      # go build -o mantis ./cmd/mantis
make test       # go test ./... -v
make vet        # go vet ./...
make fmt        # gofmt -l . (lists unformatted files)
make templates  # build, then validate every template in templates-community/
make clean      # remove built binaries and stray report files

Every package has unit test coverage, including cmd/mantis itself (flag parsing, exit codes, command wiring - main() is split into a testable dispatch()/exitCodeFor() so tests don't need to shell out to a subprocess), the DAST crawler (scope enforcement, depth/request limits, form extraction), the fuzzing engine, and the API package's generated checks (missing-auth, method-abuse, both BOLA modes, sensitive-data detection).

Configuration reference

environments.yaml maps environment names to a base URL, a security_level, and optional authentication (bearer, basic, or oauth2 client-credentials). An environment can also declare identities

  • a second set of credentials plus an owns map of resource ids, which is what turns the API BOLA check from a heuristic into a real one (see environments.example.yaml for a worked example):
application:
  name: Payments API

environments:
  dev:
    base_url: https://dev.example.com
    security_level: aggressive
  production:
    base_url: https://api.example.com
    security_level: passive
    authentication:
      type: bearer
      token: ${MANTIS_TOKEN}

Templates (templates-community/*.yaml): id, info (name/severity/tags/description/remediation/cwe/owasp), optional variables, and a requests chain. Each request has method, path, optional headers/body, matchers, and extractors. Matchers support status, word, regex, json ($.a.b[0] style paths), and dsl (status_code == 200 && contains(body, 'foo') - functions: contains, starts_with, ends_with, len, regex, to_lower, to_upper, header(name)). Variables use ${name} or {{name}} placeholders in path/headers/body.

payloads (optional) defines named value lists; attack sets the combination mode (sniper / pitchfork / clusterbomb). Each request can carry a when DSL expression to skip it conditionally based on variables from earlier steps.

flow (optional) replaces the sequential chain with an explicit script. Statement reference:

http(N)            — run request N (1-indexed); populates http<N>_status
                     (int), http<N>_body (string), http<N>_matched (bool)
if <expr>          — DSL condition; any variable or httpN_* is available
else               — optional alternative branch
end                — closes an if block
set name = <expr>  — evaluate expression and store result; {{name}} renders it
stop               — terminate the flow, keep findings collected so far
# comment          — ignored

Example:

flow: |
  http(1)
  if http1_matched
    http(2)
    if http2_status == 200 && contains(http2_body, "admin")
      http(3)
    end
  end

Smoke workflows (smoke/*.yaml): an ordered list of steps, each with a request, assertions (status, path + exists/equals, or a raw dsl expression), and optional extract. cleanup requests always run last, best-effort. Workflows can depends_on other workflows by id.

Every finding carries the request/response exchange(s) that produced it. Sensitive headers and any resolved secret values are redacted before a finding is ever constructed (see internal/httpclient/redact.go) - secrets never reach a report or log line.

Roadmap

Near-term

  • Environment drift detection - compare the same finding across dev/test/acc/prod and flag regressions introduced between environments
  • GitLab Code Quality / SAST report format (GitHub and Azure DevOps both have native reporters now; GitLab still only gets file-based SARIF/JUnit)
  • More fuzzing payloads/classes (SSRF-via-parameter, XXE, NoSQL injection) once the current 5-class set has proven itself low-noise in practice

Later

  • GraphQL support past a raw introspection template
  • WebSocket testing, browser-based/JS-driven crawling

License

MIT - see LICENSE.

About

Mantis is a template-driven DAST and application validation engine for CI/CD pipelines - smoke tests, passive/active scanning, OpenAPI-driven API checks and an environment-aware security gate, in one deterministic tool.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages