Exposure Triage
Ad-hoc search, filtering, and aggregation of the user's exposures — instances of vulnerabilities, misconfigurations, or findings discovered in their environment. Translate questions like "show me all open critical exposures breaching SLA" into the correct search*Data / aggregate*Data call with proper scoping and deep links.
An exposure is an instance (e.g., "CVE-2024-3400 on host web-01"). A vulnerability is the global CVE record. This skill is about exposures; use securin-cve-enrichment for vulnerability intel.
When to use
- "Show me all open critical exposures"
- "How many exposures are breaching SLA?"
- "Break down exposures by severity and workspace"
- "List exposures older than 90 days"
- "Exposure volume over time" / "new vs closed exposures"
- "Show me exposures tied to CVE-2024-3400"
- "Which exposures are on exposed-to-internet assets?"
Pre-flight
Step 0 — Account preflight (CC-1)
See _shared/account-preflight.md. Resolve account-id(s) and validate access before any query. If the question implies a workspace subset, resolve workspace-ids too.
Before using this skill, read every file in the references folder, including the shared references/_shared/ docs.
Step 0.5 — Detect composite vs source data model
Exposures are affected by the composite flag too — composite accounts have a separate composite-exposure index with compositeExposure.* field paths and use the exposureQuery tool (source accounts use searchExposureData + aggregateExposureData). See _shared/composite-vs-source.md. Cache the flag for the turn — picking the wrong model returns empty results with no error.
Suggested tools
Primary
searchExposureData— flat listaggregateExposureData— bucketed count (TERMS) and time-bucketed histogram (DATE_HISTOGRAM)aggregateVulnerabilityTimelineData/searchVulnerabilityTimelineData— historical vulnerability state changes over time (if available for account; fall back to DATE_HISTOGRAM if 404)
Supporting
getApiFieldswithentityType: ["EXPOSURE"]— field discoverygetGroupByFields— valid aggregation dimensionsgetTopValues— enum values for a fieldgetDefaultViewForGroupByField— platform's default column set for a groupingvalidateFilter— FQL construction + validationgetEffectiveAccessWorkspaces— workspace scoping
Deep links (CC-2)
createDeepLink(preferred) — call once per list/aggregation, plus once per bucket if you need per-bucket linksgetDeepLink— URL for a known exposure-id
See _shared/deep-links.md.
Workflow
Step 1 — Classify the ask
| Shape | Example | Tool |
|---|---|---|
| Flat list | "List open critical exposures" | searchExposureData |
| Single-field bucket | "Exposures by severity" | aggregateExposureData |
| Time series | "Exposures created per week over 6 months" | aggregateVulnerabilityTimelineData or aggregateExposureData DATE_HISTOGRAM |
| Aggregation with per-bucket deep links | "Bucket counts, clickable" | aggregateExposureData + one createDeepLink per bucket |
Step 2 — Discover fields if uncertain
getApiFields(entityType=["EXPOSURE"])
getTopValues(field="exposure.status")
getGroupByFields(entityType="EXPOSURE")
Step 3 — Compose FQL
See _shared/fql-grammar.md. Exposure-specific patterns:
exposure.status = 'Open'
exposure.scores.scoreLevel = 'Critical'
"exposure.scores.score" >= 7.0 # numeric — sort uses the same path: exposure.scores.score:desc
"exposure.firstDiscoveredOn" >= "2026-01-01T00:00:00Z"
exposure.remediationTarget.status = 'Overdue'
exposure.assignments.assignedTo.name = 'team:remediation'
exposure.mappedAttributes.vulnerabilityIds = 'CVE-2024-3400'
exposure.mappedAttributes.type like 'Vulnerability'
Cross-entity filters (exposure → vuln / asset):
# Exposures tied to a specific CVE — correct cross-entity field is vulnerabilities.id
vulnerabilities.id = 'CVE-2024-3400'
# ❌ vulnerabilities.vulnerabilityId → 400 Invalid field
# ❌ vulnerabilities.cveId → 400 Invalid field
# Exposures tied to exploited vulnerabilities
vulnerabilities.isCisaKEV = true
# Exposures on exposed-to-internet assets (source-model account)
asset.reachability = 'Exposed'
# Same, composite-model account
compositeAsset.reachability = 'Exposed'
Step 4 — Pick and call the right tool
- For time series: try
aggregateVulnerabilityTimelineData/searchVulnerabilityTimelineDatafirst. If those return 404, fall back toaggregateExposureDatawithDATE_HISTOGRAM. - For per-bucket deep links, run
aggregateExposureDatato get the buckets, then callcreateDeepLinkonce per bucket with the bucket's filter narrowed by the bucket value.
aggregateExposureData — correct request shape
function is an object with a type discriminator. Type-specific parameters (field, size, etc.) go inside the function object:
{
"filters": "exposure.status = 'Open'",
"aggs": [{
"name": "bySeverity",
"function": {"type": "TERMS", "field": "exposure.scores.scoreLevel", "size": 10}
}]
}
Common mistakes that cause 400/500:
- ❌
"function": "TERMS"— function must be an object, not a string - ❌
"apiPath": "..."— key must be"field", not"apiPath" - ❌
"field": "exposure.workspaceId"— returns 500; use"asset.workspaces.name"for workspace grouping
aggregateExposureData with DATE_HISTOGRAM — time series shape
Different structure from TERMS: nested aggs use "aggs", and interval must be Title Case. isFixedInterval, extendedBounds, and hardBounds are required:
{
"aggs": [{
"name": "openedByMonth",
"function": {
"type": "DATE_HISTOGRAM",
"field": "exposure.firstDiscoveredOn",
"interval": "Month",
"isFixedInterval": false,
"extendedBounds": {"min": "2025-11-01T00:00:00Z", "max": "now"},
"hardBounds": {"min": "2025-11-01T00:00:00Z"}
},
"aggs": [{"name": "count", "function": {"type": "COUNT", "field": "exposure.exposureId"}}]
}]
}
Valid interval values (Title Case only): "Day", "Week", "Month", "Quarter", "Year".
❌ "month", "MONTH", "1M", "MONTHLY" all return 400.
Step 5 — Deep links (CC-2)
Every result table adds a View in Platform column. Every bucket in an aggregation gets its own URL. Call createDeepLink once per reported scope; getDeepLink(exposureId) for individual drill-downs.
Step 6 — Emit response
**Query:** <plain-English restatement>
**Account:** <account name + id>
**Filter:** `<FQL>`
<results table with View-in-Platform links>
[View full list in Platform](<top-level deep link>)
**Observations:**
- <Pattern noted — e.g., "70% of critical exposures are in one workspace">
- <Suggested next step — e.g., "Drill into remediation: use securin-remediation-guidance for the top 3">
Common recipes
"Open critical exposures breaching SLA"
searchExposureData
filter: exposure.status = 'Open'
AND exposure.scores.scoreLevel = 'Critical'
AND exposure.remediationTarget.status = 'Overdue'
sort: "exposure.scores.score:desc"
"Break down exposures by severity and workspace"
Run aggregateExposureData once per dimension (two calls):
// Call 1 — by severity
{
"filters": "exposure.status = 'Open'",
"aggs": [{"name": "bySeverity", "function": {"type": "TERMS", "field": "exposure.scores.scoreLevel", "size": 10}}]
}
// Call 2 — by workspace (use asset.workspaces.name — exposure.workspaceId returns 500)
{
"filters": "exposure.status = 'Open'",
"aggs": [{"name": "byWorkspace", "function": {"type": "TERMS", "field": "asset.workspaces.name", "size": 25}}]
}
Scope guard (CC-3)
- Global CVE details → hand off to
securin-cve-enrichment. - "Am I affected by threat X" → hand off to
securin-threat-correlation. - "How do I fix this exposure" → hand off to
securin-remediation-guidance. - Pure asset questions → hand off to
securin-asset-triage. - Unknown tool needed → fall back to the platform's built-in
Securin__search_toolsmeta-tool to look up the right MCP tool by description.
Edge cases
- Empty result on cross-entity filter — check asset prefix (
asset.*vscompositeAsset.*) matches the account's data model. - Custom statuses — accounts can customize the exposure state machine; use
getTopValues(field="exposure.status")to enumerate. - Very wide time windows — lean on
aggregateExposureData(TERMS or DATE_HISTOGRAM) rather than raw listing. - Tags / custom attributes —
exposure.tags,exposure.customAttributes.*are discoverable viagetApiFields.
Visual output (CC-4)
When this skill produces aggregated or multi-row data (counts, trends, distributions, comparisons, single-CVE reports), emit a chart/graph/infographic in the Securin brand — multi-series palette (#9C66FF / #7F30FF / #E96001 / #4D268D / #DD639C / …, assigned in order), semantic CHML severity colors (Critical #A60D08 → Info #C5CBD6), Poppins headings on DM Sans body, light theme, and the Securin logo. Use the 10-stop brand purple ramp for heatmaps/sequential scales; gradients are background decoration only — never on chart bars, lines, or slices. Full color system in _shared/brand.md. Offer customization after delivery; never default to a different brand.
References
- Exposure Fields Quickref
- Shared: Account Preflight
- Shared: Composite vs Source
- Shared: Deep Links
- Shared: FQL Grammar
- Shared: Sorting Rules
- Shared: Brand & Visual Communication