Create Harness STO security exemptions (waivers) for vulnerabilities found by SAST, SCA, DAST, secret, container, or IaC scanners. Works from both entry points in the Harness UI: the Vulnerabilities tab of a specific pipeline execution (Target, Pipeline, or Project scope) and the All Issues page (Project scope only). Supports single creates with per-row error tolerance and bulk creates of up to 100 issues in one all-or-none transaction. The requester is derived automatically from the authenticated user. Use when a user wants to exempt a vulnerability, waive a CVE, suppress a security finding, mark a finding as a false positive, accept the risk, or exempt several vulnerabilities at once. Trigger phrases: exempt vuln, exempt vulnerability, exempt CVE, waive vulnerability, create exemption, suppress security issue, false positive, accept risk, ignore CVE, bulk exempt.
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Create a security exemption (waiver) for one or more Harness STO vulnerabilities. STO has two places a user can request an exemption from, and each one allows a different set of scopes:
| Where the user is in the Harness UI | Scopes they can request |
|---|---|
| Vulnerabilities tab of a specific pipeline execution | This Project, This Pipeline, This Target |
| All Issues page (the project-wide baseline) | This Project only |
The reason for the difference is simple: rows on the All Issues page are aggregated across
executions and don't carry an execution_id, so the backend has no pipeline or target context
to attach a narrower scope to. Org- and Account-scoped exemptions are not created here at all;
they're produced later by approving an existing exemption at a higher scope (via
harness_execute resource_type=security_exemption action=approve body={scope:'ORG'|'ACCOUNT'}),
which is outside the scope of this skill.
Ask the user, in plain language:
Which view are you in?
- The Vulnerabilities tab for a specific pipeline execution, or
- The All Issues page (the project-wide baseline view).
If you have the URL handy, just paste it — I'll pull out the org, project, and execution from it automatically.
You also need:
A pasted Harness UI URL is always the fastest route. The MCP url parameter auto-extracts every
identifier in the path, so the user does not have to type anything else. The shapes to recognise:
| URL the user pastes | What it tells you | Which path |
|---|---|---|
…/sto/executions/{executionId}/pipeline | The Vulnerabilities tab of one execution | Vulnerabilities path — extracts org_id, project_id, execution_id |
…/pipeline/orgs/{org}/projects/{project}/pipelines/{pipelineId}/executions/{executionId}/… | A pipeline execution detail page | Vulnerabilities path — also extracts pipeline_id |
…/sto/issues or …/sto/issues/{issueId} | The All Issues page | All Issues path — extracts org_id, project_id only |
If the user pastes a Vulnerabilities-tab URL, treat the request as the Vulnerabilities path no matter how they phrased it. If they paste an All Issues URL, do not ask for an execution ID — that view doesn't have one.
Chat is a narrow surface — a 50-row table is unreadable, and the user usually only cares about a handful of issues. Start with a short preview, tell them the total, and let them ask for more or narrow the filter.
The default opening move:
harness_list for a small page (10 rows is a good default).total — the total number of matching issues in the project /
execution — and decide what to show:
total ≤ 10, show every row.total > 10, show the first 5 sorted by severity (Critical → High → Medium → Low),
then say something like:
Showing 5 of {total} matching issues, sorted by severity. Tell me how to narrow this — for example: "show 10 mediums", "only SAST", "search log4j", "show all highs" — or pick from the list below.
harness_list with the new
filters or a larger page size. Do not paginate manually unless the user asks.harness_list
resource_type: "pipeline_security_issue"
org_id: <org>
project_id: <project>
filters:
execution_id: <execution_id> # REQUIRED
severity_codes: "Critical,High" # optional, user-driven
issue_types: "SCA,SAST" # optional
search: "<CVE or component>" # optional
include_exempted: false # hide rows that already have an exemption
page_size_existing: 10 # small preview — bump only on user request
page_size_new: 10The response flattens the existing and new diff partitions into a single items[] and tags
each row with _partition. Each row exposes issue_id and targetVariantName (a display string
like "nodegoat:master"). It does not expose the raw target_id or pipeline_id — Step 3
covers how to look those up when the user picks a narrower scope.
harness_list
resource_type: "security_issue"
org_id: <org>
project_id: <project>
filters:
severity_codes: "Critical,High"
issue_types: "SCA,SAST"
search: "<CVE or component>"
exemption_statuses: "None" # hide rows that already have an exemption
size: 10 # small preview — bump only on user requestWhen you ask the user which issues to exempt, always include three things in the same prompt:
Phrase it conversationally, for example:
Which issues would you like to exempt? Pick by number (e.g.
3,4-7,3,5,9) or paste issue IDs. You can select up to 100 at a time — that's the per-batch limit for bulk exemptions. If you want more than 100, narrow the filter first or split into multiple batches.
Do not offer "exempt all" as a shortcut. The selection should always be a deliberate list of numbers or IDs — see the 100-issue cap in Step 5b for the full rationale.
Offer one of:
Two rules the backend enforces, so this skill must enforce them too before calling
harness_create:
target_id and pipeline_id are mutually exclusive on a single exemption. Never send both.project_id on the request.pipeline_security_issue rows do not carry target_id or pipeline_id, so you need to
resolve them yourself for the narrower scopes. Use the recipes below — do not poke at unrelated
endpoints.
pipeline_idpipeline_id. Use it directly — do not look it up again.execution_id), fetch the execution once:
harness_get
resource_type: "execution"
resource_id: <execution_id>
org_id: <org>
project_id: <project>pipelineIdentifier off the response.Never list pipelines and search for a match. The execution-to-pipeline link is 1:1, and the lookup above is constant-time.
target_idPull the scan steps for the same execution and build a small lookup map:
# 1. One call returns scanner + target metadata for every step in the execution.
harness_list
resource_type: "pipeline_security_step"
org_id: <org>
project_id: <project>
filters:
execution_id: <same execution_id used for the issue list>
# 2. Build a map from the response:
# key = "<targetName>:<targetVariant>" e.g. "nodegoat:master"
# value = targetId (22-character Harness ID)For each selected issue row, look its targetVariantName up in that map. The matching targetId
is what goes into body.target_id (single create) or each item's target_id (bulk create).
The pipeline_security_issue list response also embeds _target_id_lookup_hint and
_pipeline_id_lookup_hint fields that describe these same recipes — surface them verbatim to
the user if they ask why the extra call is needed.
When the user starts from a URL, the cheapest deterministic plan is:
harness_list pipeline_security_issue — always, so the user can pick.harness_list pipeline_security_step — only if This Target scope is in play.harness_get execution — only if This Pipeline scope is in play and you don't already have
pipeline_id from the URL.harness_create security_exemption or security_exemption_bulk.That's three reads at most, plus one write. Never more.
The only scope available is This Project. If the user asks for This Pipeline or This Target, explain the limitation and wait for them to choose — don't quietly fall back to Project scope:
The All Issues page doesn't know which execution surfaced this issue, so I can only create a Project-scoped exemption from here. If you need a narrower scope, open the Vulnerabilities tab for a recent execution of the pipeline that scanned this target, send me that URL or execution ID, and I'll scope the exemption to just that pipeline or target.
| Field | Required | Notes |
|---|---|---|
issue_id | yes | From the selected row. |
type | yes | One of: Compensating Controls, Acceptable Use, Acceptable Risk, False Positive, Fix Unavailable, Other. These match the choices in the Harness UI's Request Exemption dialog. |
reason | yes | A short justification (max 1024 characters). Ask if the user didn't give one — never invent it. |
duration_days | no | Defaults to 30. Confirm if the user mentioned a different number. |
link | no | A ticket URL (Jira, GitHub issue, etc.) if the user mentions one. |
occurrences | no | A list of specific occurrence IDs. Use only when the user wants to exempt some occurrences and not the whole issue. |
scan_id | no | Exempts every occurrence found in one scan. Requires This Target scope. |
requester_id is filled in automatically from the authenticated PAT — never ask the user for it.
If you ever need the full schema, run harness_describe(resource_type="security_exemption"). Do
the same if a create call returns a missing-field error.
Echo the payload back to the user in plain text before calling harness_create. Use the same
shape every time so users learn to scan it quickly:
About to create this exemption:
Issue: <id>
Scope: This Project | This Pipeline (<pipeline_id>) | This Target (<target_id>)
Type: <type>
Reason: "<reason>"
Duration: <n> days
Proceed? (yes/no)On yes, call:
harness_create
resource_type: "security_exemption"
org_id: <org>
project_id: <project>
body:
issue_id: <id>
type: <type>
reason: <reason>
duration_days: <n> # optional
# exactly one of the next two, or NEITHER for This Project scope:
pipeline_id: <pipeline_id> # This Pipeline scope only
target_id: <target_id> # This Target scope only
link: <url> # optional
occurrences: [ids] # optional
scan_id: <scan_id> # optional, This Target scope onlyPick one of the two paths below and tell the user which one you're taking and why:
| Path | Use when | How to call it | Behaviour |
|---|---|---|---|
| Single (loop) | Items need different type, reason, or duration_days, or the user wants every row to succeed or fail on its own. | One harness_create resource_type=security_exemption per row. | Each row is its own transaction. Row 3 can succeed even if row 4 fails. |
| Bulk | Every item shares the same type, reason, and duration_days — even if the scope differs per item. | One harness_create resource_type=security_exemption_bulk with up to 100 items. | All-or-none. If any item fails validation or insert, the whole batch is rolled back. One DB transaction, one audit row. |
Default to bulk when two or more issues share the batch-level fields. This is the common case ("exempt all these Log4j highs"). Per-item scope can still vary — a single bulk call can mix This Project, This Pipeline, and This Target items as long as the type and reason are the same.
Default to single when items have different justifications or types, or when the user explicitly wants per-row tolerance.
| Field | Where it sits in the body | Must be identical for every item? |
|---|---|---|
org_id, project_id | request scope | Yes — one batch = one project. |
type, reason, duration_days, link, expiration | top-level body | Yes — one value applied to all items. |
issue_id | per item | One per item, required. |
target_id or pipeline_id | per item | Each item picks its own scope. |
scan_id, occurrences, search | per item | Independent per item. |
If the user wants different type or reason values per row, bulk cannot encode that — use
single creates.
The bulk endpoint accepts at most 100 items per call. The MCP preflight enforces this and will
throw items must contain at most 100 entries before the request is sent. Apply these rules:
total from the prior harness_list response. If
total > 100, do not present "exempt all" as a default. Show the count and ask the user to
either narrow the filter (severity, scanner, search term, target) or explicitly opt into
chunking.succeeded / failed counts
per chunk in the summary.total. Wait for them to
acknowledge the number before proceeding.When you select rows for the user, ask them to choose by number or issue ID, not by typing "all". Phrase the prompt as something like:
Which issues would you like to exempt? Pick by number (e.g.
1,3,5-7) or paste issue IDs.
If the user still says "all" and the count is over 100, surface the rule from point 1 above.
harness_create
resource_type: "security_exemption_bulk"
org_id: <org>
project_id: <project>
body:
type: <type> # applied to every item
reason: <reason> # applied to every item
duration_days: <n> # optional, default 30, applied to every item
link: <url> # optional, applied to every item
items: # 1..100 entries
- issue_id: <id1>
# per item, optional: target_id XOR pipeline_id, scan_id, occurrences, search
- issue_id: <id2>
pipeline_id: <pipeline_id>
- issue_id: <id3>
target_id: <target_id>Per-item rules:
target_id and pipeline_id are mutually exclusive within a single item.target_id or pipeline_id — the whole batch is
Project-scoped.The response carries a top-level status banner. Surface it verbatim and react accordingly:
status | What it means | What to do |
|---|---|---|
ALL_SUCCEEDED | Every item was created. | Report each {issueId, id} and note that all are Pending approval. |
ALL_FAILED | The whole batch was rolled back, and every item carries the same error. | Surface results[0].error, fix the root cause, then resubmit the full corrected list — never retry only the failed rows. |
MIXED_UNEXPECTED | The server returned both successes and failures, which violates the all-or-none contract. | Treat this as a server bug. Show the raw results[], do not auto-retry. |
EMPTY | No results in the response. | Treat as a failure; check that the request actually included items. |
Typical causes of ALL_FAILED are: an unknown issue_id, no scan found for the issue within
the project scope, a mutual-exclusion slip, or a duplicate exemption. results[0].error names
the actual cause.
If a single bad item is poisoning a large batch and the user wants to know which one, you may fall back to single creates to isolate it — but ask first; don't switch strategies silently.
For each created exemption, return:
exemption_idstatus — this will be Pending until someone with the right permissions approves it.
Approval is a separate workflow and is outside the scope of this skill./ng/account/{accountId}/all/orgs/{org}/projects/{project}/sto/exemptionsIf any create call failed, surface the API error verbatim — it's almost always a mutual-exclusion violation or a missing field, and the message tells the user exactly what to fix.
User: "Exempt CVE-2024-1234 from execution ehsPKtczTRO5CUDAt-NR for that pipeline only.
False positive — already patched in our fork."
harness_list pipeline_security_issue with execution_id and search: "CVE-2024-1234".issue_id.pipeline_id — either from the pasted URL, or via harness_get execution.yes, then harness_create with
body: { issue_id, type: "False Positive", reason: "Patched in our fork — see PR #422", pipeline_id }.User: "From the All Issues page, suppress these three high-severity Log4j findings for 90 days. Acceptable risk — we've mitigated at the gateway."
harness_list security_issue with severity_codes: "High", search: "log4j".issue_ids.target_id or pipeline_id on any item).harness_create resource_type=security_exemption_bulk with
body: { type: "Acceptable Risk", reason: "Mitigated at API gateway WAF — ticket SEC-880", duration_days: 90, items: [{issue_id:"i1"}, {issue_id:"i2"}, {issue_id:"i3"}] }.status: ALL_SUCCEEDED; report the three returned exemption IDs.User: "From execution ehsPKtczTRO5CUDAt-NR, exempt these five Critical issues — the first
two for the whole project, the next two only on this pipeline, the last one only on the
api-gateway target. False positive across the board."
harness_list pipeline_security_issue with execution_id and severity_codes: "Critical".harness_list pipeline_security_step (same execution) to resolve target_id for the
api-gateway row.type and reason, and per-item scope is encoded per row:
harness_create resource_type=security_exemption_bulk body:
type: "False Positive"
reason: "False positive — see triage doc #441"
items:
- { issue_id: i1 } # This Project
- { issue_id: i2 } # This Project
- { issue_id: i3, pipeline_id: <pipelineId> } # This Pipeline
- { issue_id: i4, pipeline_id: <pipelineId> } # This Pipeline
- { issue_id: i5, target_id: <targetId> } # This TargetUser: "Exempt these two CVEs — the Log4j one is a False Positive, the Spring4Shell one is Acceptable Risk."
The two items need different type values, which bulk cannot encode. Use the single path: two
sequential harness_create resource_type=security_exemption calls. Splitting into two batches
of one is just the single path with extra ceremony.
User: "From the All Issues page, exempt this XSS finding for the web-frontend target only."
Don't quietly downgrade. Say:
The All Issues page doesn't know which execution found this issue, so I can only create a Project-scoped exemption from here. Two options:
- Create it as a This Project exemption now, or
- Open the Vulnerabilities tab for a recent execution that scanned
web-frontend, send me that URL or execution ID, and I'll scope it to just that target.
If the user supplies both pipeline_id and target_id for the same exemption, push back before
calling the API:
An exemption can be scoped to one pipeline or one target, not both. Pick one: This Pipeline applies across every target this pipeline scans, while This Target applies across every pipeline that scans this target.
pipeline_security_issue uses diff pagination — page_existing / page_size_existing and
page_new / page_size_new are independent counters. Open with a small preview (10 of each)
and only bump the size when the user asks for more or narrows the filter. The response always
reports the partition-aware total, so you can show "5 of 19" without paging further.security_issue uses standard pagination (page / size). Same pattern — start with size: 10, surface total, expand on request.include_exempted: false on
pipeline_security_issue, or exemption_statuses: "None" on security_issue.risk: high_write, retryPolicy: do_not_retry — surface failures to the user instead of
auto-retrying.sto-core/docs/STO-8977-bulk-exemption-api.md). Prefer bulk whenever two or more items share
the batch-level fields.ALL_FAILED or
MIXED_UNEXPECTED response — resubmit the full corrected list instead.| Symptom | Cause | Fix |
|---|---|---|
Missing required fields for security_exemption: issue_id, type, reason | A required field is absent from body. | Ask the user — never invent a reason or default a type. |
API error: targetId cannot be combined with pipelineId / projectId | Both scope fields were sent on one exemption. | Pick exactly one — see the rules in Step 3. |
List error: account scope not supported | The caller passed resource_scope='account' or 'org' when listing exemptions. | security_exemption is always listed at project scope. Those keywords are approval scopes (used with harness_execute), not list scopes. Remove the override and re-list. |
A new exemption shows up as Approved immediately | The requester's RBAC grants auto-approval at that scope. | This is working as designed; mention it in the report. |
getCurrentUserId failure | The PAT-derived user lookup failed (network or auth). | Surface the error verbatim — never fabricate an ID. Check that HARNESS_API_KEY is still valid. |
| User asks for Org- or Account-scope on creation | The create endpoint can't do that. | Create at This Project scope, then promote it via `harness_execute resource_type=security_exemption action=approve body={scope:'ORG' |
| User started on the All Issues page but now wants This Pipeline or This Target | The All Issues path is Project-only by design. | Switch to the Vulnerabilities tab path with a relevant execution_id. |
The agent keeps hunting for target_id on issue rows | pipeline_security_issue doesn't expose target_id — only targetVariantName (a display string). | Call harness_list pipeline_security_step once with the same execution_id, build a {targetName:targetVariant → targetId} map, then look up each row's targetVariantName. The list response embeds _target_id_lookup_hint with this same recipe. |
Bulk response status: ALL_FAILED | One or more items failed validation or insert; the whole batch was rolled back per the all-or-none contract. | Read results[0].error for the real cause (unknown issue_id, no matching scan in scope, mutual-exclusion slip, duplicate exemption). Fix it and resubmit the full corrected list — never retry only the failed rows. |
Bulk response status: MIXED_UNEXPECTED | The server returned both successes and failures, which violates the all-or-none contract. | Treat as a server-side bug. Show the raw results[], do not auto-retry. Fall back to single creates for the failed items only after the user acknowledges. |
Preflight error: items must contain at most 100 entries | The caller exceeded the per-batch limit. | Split into multiple bulk calls of 100 or fewer items each. Prevent this by checking total from the prior list before offering "exempt all" — see the 100-issue cap in Step 5b. Never silently truncate. |
Preflight error: items[N].issue_id or items[N] sets both target_id and pipeline_id | One item in the batch is malformed. | The index in the error message tells you which item. Fix that one, then resubmit the full list. |
harness/harness-ai
ca2ab81
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.