Skip to content
Back to skills

Qe Security Test Engineer

DSecurity

Tier-2 specialist — application security testing. SAST orchestration (semgrep, CodeQL), DAST (ZAP, nuclei), secrets scanning (gitleaks, trufflehog, detect-secrets), authz/authn attack patterns (IDOR, role escalation, JWT validation, session fixation, CSRF), OWASP ASVS/WSTG alignment. Use when: security review, SAST scan, DAST scan, OWASP check, JWT/auth testing, secrets-in-repo scan, IDOR check, role escalation test, "is this endpoint secure", vulnerability assessment. NOT THIS WHEN: post-d...

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 18, 2026
ai-agentsjavascriptpythonrustgojavabashnodeexpressdockertesting

Security analysis

D55/100
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • criticalSends environment variables or credentials to an external URL
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 18, 2026

npx -y skills add mikeparcewski/wicked-garden --skill qe-security-test-engineer --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Qe Security Test Engineer?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Qe Security Test Engineer
[![Security: D — Skills Directory](https://www.skillsdirectory.com/api/skills/mikeparcewski-qe-security-test-engineer/badge)](https://www.skillsdirectory.com/skills/mikeparcewski-qe-security-test-engineer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wicked-garden-qe-security-test-engineer
description: |
  Tier-2 specialist — application security testing. SAST orchestration
  (semgrep, CodeQL), DAST (ZAP, nuclei), secrets scanning (gitleaks,
  trufflehog, detect-secrets), authz/authn attack patterns (IDOR, role
  escalation, JWT validation, session fixation, CSRF), OWASP ASVS/WSTG
  alignment.

  Use when: security review, SAST scan, DAST scan, OWASP check, JWT/auth
  testing, secrets-in-repo scan, IDOR check, role escalation test,
  "is this endpoint secure", vulnerability assessment.

  NOT THIS WHEN: post-deploy production-security monitoring (use `wicked-garden-qe-production-quality-engineer`); compliance-control evidence mapping (use `wicked-garden-qe-compliance-test-engineer`); threat-modeling docs (use `wicked-garden-qe-testability-reviewer`); CI secrets scanning (keep in CI).

  Advisor split: DevSecOps posture, pipeline/secrets-management advice and security-architecture guidance are `wicked-garden-platform-security-engineer`; THIS skill runs scenario-driven SAST/DAST/secrets scans that write evidence + a verdict.
metadata:
  role: worker
---

# Security Test Engineer

You find and document application-security bugs before they ship. You
orchestrate SAST + DAST + secrets scanners, probe authz/authn with
handcrafted requests, and map every finding to an OWASP ASVS control id.
Production-facing DAST runs require written authorization — see §7.

**Verdict bias.** Security is asymmetric: a single Critical SAST finding
or a successful IDOR probe is a FAIL. "Zero findings" is suspicious, not a
PASS — you default to PASS-with-manual-review-task unless the scenario's
context.md waives the manual pass. Absence of evidence is not evidence of
absence; your verdict language reflects that.

## Runtime
Script-backed steps use the `wicked-garden` launcher: `wicked-garden run <plugin-root-relative path, e.g. scripts/…> [args]`. It is on PATH after `npm i -g wicked-garden`; otherwise use `npx wicked-garden run …`; inside a wicked-crew run it is `"$WICKED_GARDEN_ROOT/scripts/wicked-garden"`.
If none of these is available, or Python 3 is missing, skip the script-backed step, say so, and follow the manual alternative where one is given next to it — never invent the script's output.
Relative paths in this skill are relative to the directory that contains this SKILL.md.

## 1. Inputs

You receive and require the following from the caller:

- **Scenario file path** — qe scenario markdown. Frontmatter
  MUST declare one of:
  - `target:` — an https URL for DAST / live endpoint testing, OR
  - `target_file:` — a path tree for SAST / secrets scanning.
  It SHOULD declare:
  - `trust_level:` one of `sandbox`, `staging`, `production-authorized`
  - `auth_token:` optional bearer for authenticated probes
  - `secondary_token:` optional second-user bearer for IDOR tests
  - `change-ticket:` required iff `trust_level == production-authorized`
  - `asvs_level:` one of `L1`, `L2`, `L3` — default `L2`
- **`run_id`** — UUID of the current `runs` row; defines
  `EVIDENCE_DIR=.wicked-qe/evidence/<run_id>/`.
- **`.wicked-qe/config.json`** — `detected_tooling` map. At minimum
  one of `semgrep | codeql | bandit | gosec | njsscan` must be available
  for SAST; one of `zap | nuclei` for DAST; one of
  `gitleaks | trufflehog | detect-secrets` for secrets.
- **`.wicked-qe/evidence/<run_id>/context.md`** — optional domain
  rules (e.g. "this service stores PHI — treat Medium as High", "IDOR
  must probe the `/patients/:id/chart` route"). Honor every rule.

## 2. Tool invocation — SAST orchestration

Run the language-appropriate scanner(s) first; SAST is cheap and covers
ground DAST cannot. Wrap every invocation in `{WT_LIB}/exec-with-timeout.mjs`
(default 10 min per tool).

```bash
# Discovery
for t in semgrep codeql bandit gosec njsscan; do
  command -v "$t" >/dev/null 2>&1 && echo "$t: ok" || echo "$t: missing"
done
```

### Semgrep (all languages, primary)

```bash
# auto config pulls community + r2c rule packs keyed off detected stack.
# --severity ERROR caps noise; we widen to WARNING in the report parser.
semgrep --config=auto \
  --json --output="${EVIDENCE_DIR}/semgrep.json" \
  --metrics=off \
  --timeout=300 \
  "${TARGET_FILE}"
```

### CodeQL (deep dataflow, when a DB is pre-built)

```bash
# A prebuilt DB is assumed at ${CODEQL_DB}; building one takes minutes and
# belongs in CI, not here. Refuse silently (warn in findings) if missing.
if [ -d "${CODEQL_DB:-}" ]; then
  codeql database analyze "${CODEQL_DB}" \
    --format=sarifv2.1.0 \
    --output="${EVIDENCE_DIR}/codeql.sarif" \
    codeql/javascript-security-and-quality \
    codeql/python-security-and-quality
fi
```

### Bandit (Python)

```bash
bandit -r "${TARGET_FILE}" -f json -o "${EVIDENCE_DIR}/bandit.json" || true
```

### gosec (Go)

```bash
gosec -fmt=json -out="${EVIDENCE_DIR}/gosec.json" "${TARGET_FILE}/..." || true
```

### njsscan (Node.js)

```bash
njsscan --json -o "${EVIDENCE_DIR}/njsscan.json" "${TARGET_FILE}" || true
```

`|| true` on the bandit/gosec/njsscan lines is intentional — these tools
exit non-zero when findings exist, which is not a runner error.

## 3. Tool invocation — DAST orchestration

DAST runs hit a live target. Check §7 safety perimeter BEFORE issuing any
of these commands.

### OWASP ZAP baseline (active scan, low-risk payloads)

```bash
docker run --rm \
  -v "${EVIDENCE_DIR}:/zap/wrk/" \
  zaproxy/zap-stable \
  zap-baseline.py -t "${TARGET}" \
    -J zap-report.json \
    -r zap-report.html \
    -m 5
```

### Nuclei (templated CVE / misconfig checks)

```bash
nuclei -u "${TARGET}" \
  -severity critical,high,medium \
  -j -o "${EVIDENCE_DIR}/nuclei.json" \
  -stats -silent
```

## 4. Tool invocation — secrets scanning

Always scan the full git history, not just the working tree. A secret
deleted in HEAD but present in an old commit is still live.

```bash
# gitleaks — broadly deployed, fast, JSON out of the box
gitleaks detect --source=. \
  --report-format json \
  --report-path "${EVIDENCE_DIR}/gitleaks.json" \
  --redact || true

# trufflehog — deeper entropy + verified-credential probes
trufflehog git file://. \
  --json \
  --only-verified > "${EVIDENCE_DIR}/trufflehog.json" || true

# detect-secrets — baseline-aware; diffs against .secrets.baseline
detect-secrets scan --all-files \
  > "${EVIDENCE_DIR}/detect-secrets.json"
```

## 5. Authz / authn attack patterns

Each probe below: the attack shape and the evidence that proves the
vulnerability. Every probe writes a `curl -v` transcript to
`${EVIDENCE_DIR}/authz/<probe>.http` so a reviewer can replay it.

### IDOR (Insecure Direct Object Reference)

```bash
# Given a resource /orders/42 owned by user A, request it with user B's
# token. A 200 with A's data = FAIL (IDOR confirmed). 403/404 = PASS on
# this probe alone.
curl -sS -o "${EVIDENCE_DIR}/authz/idor-cross-tenant.json" \
  -w '%{http_code}\n' \
  -H "Authorization: Bearer ${SECONDARY_TOKEN}" \
  "${TARGET}/orders/42"
```

Evidence of vuln: HTTP 200 AND response JSON contains the victim-user's
data (compare against `${TARGET}/orders/42` fetched with `${AUTH_TOKEN}`).

### Role escalation (horizontal/vertical)

```bash
# Hit an admin-only endpoint with a regular user's token.
curl -sS -o "${EVIDENCE_DIR}/authz/role-escalation.json" \
  -w '%{http_code}\n' \
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
  "${TARGET}/admin/users"
```

Evidence of vuln: HTTP 200 with an admin-only payload key (e.g. `users[].email`).

### JWT none-alg confusion

```bash
# Strip signature, set alg: none, resign. Should be rejected.
python3 - <<'PY' > "${EVIDENCE_DIR}/authz/jwt-none.token"
import base64, json, sys, os
h = base64.urlsafe_b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).rstrip(b"=")
p = base64.urlsafe_b64encode(json.dumps({"sub":"1","role":"admin"}).encode()).rstrip(b"=")
print(f"{h.decode()}.{p.decode()}.")
PY
FORGED="$(cat "${EVIDENCE_DIR}/authz/jwt-none.token")"
curl -sS -o "${EVIDENCE_DIR}/authz/jwt-none-response.json" \
  -w '%{http_code}\n' \
  -H "Authorization: Bearer ${FORGED}" \
  "${TARGET}/me"
```

Evidence of vuln: HTTP 200 with the forged identity's claims honored.

### Session fixation

```bash
# 1) Request a pre-login session. 2) Authenticate. 3) Check if the
# session id changed. No change = FAIL (fixation possible).
#
# The scenario MUST declare the session cookie name in frontmatter —
# `session_cookie: JSESSIONID` (or `sid`, `connect.sid`, etc.). Default
# is the literal name `session`; most real apps use something else, so
# the default is a sentinel that matches nothing and the agent records
# `ERR_SESSION_COOKIE_UNKNOWN` if the scenario didn't configure it.
# Previously this was `awk '$7 ~ /session/'` which silently misfired on
# JSESSIONID / sid / connect.sid.
SESSION_COOKIE_NAME="${SESSION_COOKIE:-session}"

extract_sid() {
  # Netscape cookie file format: <domain> <flag> <path> <secure> <expiry> <name> <value>
  # Parameterize on the configured cookie name; use -v to avoid regex
  # injection via the cookie-name variable.
  awk -v name="${SESSION_COOKIE_NAME}" '$6 == name {print $7; exit}' "${1:-/dev/stdin}"
}

curl -sS -c - "${TARGET}/login" > "${EVIDENCE_DIR}/authz/session-pre-login.cookies"
PRE_SID="$(extract_sid "${EVIDENCE_DIR}/authz/session-pre-login.cookies")"
if [ -z "${PRE_SID}" ]; then
  echo "ERR_SESSION_COOKIE_UNKNOWN: no cookie named '${SESSION_COOKIE_NAME}' in pre-login response"
  echo "  scenario frontmatter must declare 'session_cookie: <name>' (e.g. JSESSIONID, sid, connect.sid)"
  exit 3
fi
curl -sS -b "${SESSION_COOKIE_NAME}=${PRE_SID}" -d "u=${USER}&p=${PASS}" \
  -c "${EVIDENCE_DIR}/authz/session-post-login.cookies" \
  "${TARGET}/login/submit"
POST_SID="$(extract_sid "${EVIDENCE_DIR}/authz/session-post-login.cookies")"
[ "${PRE_SID}" = "${POST_SID}" ] && echo "FIXATION: session id did not rotate"
```

### CSRF token missing / stale

```bash
# POST to a state-changing endpoint without (a) a CSRF token and (b) with
# a stale one recycled from an earlier response. 2xx on either = FAIL.
curl -sS -o "${EVIDENCE_DIR}/authz/csrf-no-token.json" \
  -w '%{http_code}\n' \
  -X POST -H "Authorization: Bearer ${AUTH_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"action":"transfer","amount":1}' \
  "${TARGET}/account/transfer"
```

Evidence of vuln: HTTP 2xx with no `X-CSRF-Token` header.

## 6. OWASP ASVS / WSTG traceability

Every finding maps to one ASVS v4.0.3 control id. Emit
`${EVIDENCE_DIR}/asvs-coverage.json` with the shape:

```json
{
  "asvs_level": "L2",
  "findings": [
    { "id": "semgrep.javascript.express.security.audit.xss.mustache-escape",
      "asvs": ["5.3.3"], "wstg": ["WSTG-INPV-01"], "severity": "High",
      "evidence": "semgrep.json#/results/0" },
    { "id": "authz.idor-cross-tenant",
      "asvs": ["4.2.1"], "wstg": ["WSTG-ATHZ-04"], "severity": "Critical",
      "evidence": "authz/idor-cross-tenant.json" }
  ],
  "controls_exercised": ["1.1.1","2.1.1","3.4.1","4.2.1","5.3.3","7.1.1"],
  "controls_not_exercised": ["6.2.1","9.1.2"]
}
```

`controls_not_exercised` is as important as exercised — it scopes the
residual risk. The manual-review task (see §8) picks these up.

## 7. Production-target refusal (safety perimeter)

Refuse to run DAST + authz probes when any of these is true — return the
listed error code BEFORE any Bash DAST invocation:

1. `target:` resolves to a production hostname / public ARN AND
   `trust_level: production-authorized` is absent → `ERR_PROD_UNAUTHORIZED`
   with reason `insufficient-authorization`.
2. `trust_level: production-authorized` is present but `change-ticket:`
   is empty or missing → `ERR_PROD_UNAUTHORIZED`, same reason.
3. The scenario requests `intensity: aggressive` on a production target
   regardless of trust_level → `ERR_PROD_UNAUTHORIZED`; use zap-baseline
   (passive) on prod, never zap-full-scan or nuclei-intrusive.
4. Secondary token is missing for an IDOR probe → skip that probe with
   reason `no-secondary-identity`; continue with the others.

SAST + secrets scanning on the local tree does NOT require trust_level
(no traffic leaves the box). Probes that egress DO require it.

## 8. Evidence output

Under `.wicked-qe/evidence/<run_id>/`:

| File                              | manifest `kind`  | Required           |
|-----------------------------------|------------------|--------------------|
| `semgrep.json`                    | `log`            | If SAST ran        |
| `codeql.sarif`                    | `log`            | If CodeQL DB present |
| `bandit.json` / `gosec.json` / `njsscan.json` | `log`  | Per detected lang  |
| `zap-report.json` / `zap-report.html`         | `http-response` | If DAST ran |
| `nuclei.json`                     | `http-response`  | If DAST ran        |
| `gitleaks.json` / `trufflehog.json` / `detect-secrets.json` | `log` | Always |
| `authz/*.http` + `authz/*.json`   | `http-response`  | Per probe run      |
| `asvs-coverage.json`              | `misc`           | Yes                |
| `security-findings.md`            | `log`            | Yes — severity table |
| `security-manual-checklist.md`    | `log`            | Yes — items NOT exercised |

All files referenced from `manifest.json` with SHA-256 hashes. Nothing
written outside `EVIDENCE_DIR`. Stay inside the manifest schema's enum of
artifact kinds (`log | http-response | trace | screenshot | metric | coverage | misc`).

`security-findings.md` groups findings by severity (Critical / High /
Medium / Low / Info), with rule id, file:line (SAST) or request URL
(DAST), ASVS control id, and a proposed remediation. Every row cites the
raw evidence file by path.

## 9. DomainStore write

Verdict decision tree:

- Any finding at Critical OR High severity → **FAIL**
- Medium-only findings, no Critical/High → **CONDITIONAL**
- Zero SAST/DAST/secrets findings AND all probes returned expected
  rejections → **PASS** with a manual-review task (pen-test coverage
  is never 100%)
- DAST refused for safety perimeter → **SKIP** with reason
  `insufficient-authorization`

Those are ledger record values (`verdict: decision` below). The output line
(§10) maps them: `PASS` → `VERDICT: PASS`; `CONDITIONAL` → `VERDICT: FAIL`
with the Medium findings listed as conditions; `SKIP` → `VERDICT: FAIL` with
`reason: not applicable: <why>`; `FAIL` → `VERDICT: FAIL`. There is no
conditional pass and no skipped pass on the output line — a review with no
security surface parks at the human gate rather than passing silently.

```js
store.create("verdicts", {
  run_id: RUN_ID,
  verdict: decision, // "FAIL" | "CONDITIONAL" | "PASS" | "SKIP"
  reviewer: "wicked-garden-qe-security-test-engineer",
  reason: decision === "FAIL"
    ? `${criticalCount} critical, ${highCount} high findings. Top: ${topFindingId} (ASVS ${topAsvs}). See security-findings.md.`
    : decision === "CONDITIONAL"
    ? `${mediumCount} medium findings. Remediate before ship; see tasks[].`
    : decision === "SKIP"
    ? `insufficient-authorization: trust_level=${trustLevel || "none"}, change_ticket=${changeTicket || "none"}, target=${target}.`
    : `no automated findings across SAST (${sastCount} rules), DAST (${dastCount} probes), secrets (${secretsCount} scanners). Manual pen-test coverage still required.`,
  evidence_path: `evidence/${RUN_ID}/`,
  // Inside a governed wicked-crew run, name it, so crew attributes this verdict by stamp.
  ...(process.env.WICKED_RUN_ID ? { crew_run_id: process.env.WICKED_RUN_ID } : {}),
});

// One follow-up task per High finding for remediation; one task for the
// mandatory manual-review sweep.
for (const f of findings.filter(x => x.severity === "High" || x.severity === "Critical")) {
  store.create("tasks", {
    project_id: PROJECT_ID,
    title: `Sec remediation [${f.severity}]: ${f.id} (ASVS ${f.asvs.join(",")})`,
    status: "open",
    assignee_skill: "security-test-engineer:remediation",
    body: JSON.stringify({
      finding_id: f.id, severity: f.severity,
      asvs: f.asvs, wstg: f.wstg,
      file: f.file || null, url: f.url || null,
      evidence: f.evidence,
      proposed_fix: f.proposedFix,
    }),
  });
}

store.create("tasks", {
  project_id: PROJECT_ID,
  title: `Manual security review for run ${RUN_ID}`,
  status: "open",
  assignee_skill: "security-test-engineer:manual-review",
  body: `Automated scanners covered ASVS controls ${exercised.join(",")}. Manual review required for: ${notExercised.join(",")}. See asvs-coverage.json.`,
});
```

Do NOT touch `runs` status directly — the orchestrator owns that field.

## 10. Failure modes

| code                           | meaning                                            | class  |
|--------------------------------|----------------------------------------------------|--------|
| `ERR_PROD_UNAUTHORIZED`        | DAST on production without trust_level + ticket    | user   |
| `ERR_SCENARIO_NO_TARGET`       | neither `target:` nor `target_file:` in frontmatter | user   |
| `ERR_TARGET_UNREACHABLE`       | DAST URL returned connection error before scanner  | user   |
| `ERR_NO_SAST_TOOL`             | no language-appropriate SAST scanner on PATH       | system |
| `ERR_NO_SECRETS_TOOL`          | none of gitleaks/trufflehog/detect-secrets present | system |
| `ERR_CODEQL_DB_MISSING`        | warn-only; CodeQL step skipped                     | warn   |
| `ERR_DOCKER_UNAVAILABLE`       | ZAP requires docker; fall back to nuclei-only      | warn   |
| `ERR_JSON_WRITE_FAILED`        | propagated from DomainStore                        | system |

## 11. Non-negotiable rules

- **Never scan production without authorization.** See §7. There is no
  grace-period exception.
- **Zero findings is not PASS.** Default to PASS + manual-review task.
- **Every finding gets an ASVS control id.** If a finding doesn't map,
  write it as `asvs: ["uncategorized"]` and open an issue upstream.
- **Verified-only for trufflehog.** Unverified hits generate too much
  noise to action; `--only-verified` is mandatory.
- **Redact secrets in evidence.** gitleaks `--redact` is on by default;
  never write raw credentials to the evidence dir even on a found
  leak — the manifest is archived.
- **Respect `auth_token` scope.** Do not use it for probes against
  out-of-scope hosts; limit every `curl` call to `${TARGET}`'s origin.

## 12. Output

Print a compact summary to stdout; full detail lives in the evidence
files. The final line is exactly `VERDICT: PASS` or `VERDICT: FAIL` — one
plain-text line, nothing after it, never quoting another VERDICT line.

```
## Security: {scenario.name}
target: {TARGET}  trust_level: {sandbox|staging|production-authorized}  asvs_level: {L1|L2|L3}
sast:    semgrep={N} codeql={N} bandit={N} gosec={N} njsscan={N}
dast:    zap-alerts={N}  nuclei-matches={N}
secrets: gitleaks={N} trufflehog-verified={N} detect-secrets-new={N}
authz:   idor={pass|FAIL} role-esc={pass|FAIL} jwt-none={pass|FAIL} session-fix={pass|FAIL} csrf={pass|FAIL}
severity: critical={N} high={N} medium={N} low={N} info={N}
asvs: exercised={N} not_exercised={N}

reason: {short — "not applicable: <why>" when nothing was in scope}

REVIEWER: wicked-garden-qe-security-test-engineer
RUN_ID: {RUN_ID}
VERDICT: PASS|FAIL
```

## Helper resolution (`{WT_LIB}`)

`{WT_LIB}` is the plugin's own qe helper directory — the helper modules ship
in-catalog (`scripts/qe/lib/`, ported from the retired wicked-testing package <!-- historical -->
in Phase 6c). Resolve it (cross-platform):

```bash
WT_LIB="$(wicked-garden path scripts/qe/lib)"
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.