Asset Triage
Purpose
Ad-hoc asset search, filtering, and aggregation. Translate natural-language questions about the user's asset inventory ("show me exposed-to-internet prod assets with critical exposures") into the correct Securin MCP search*Data / aggregate*Data call, with proper account scoping and deep links back to the platform.
When to use
- "Find all Linux servers in
prod-cloudworkspace" - "How many assets do I have by cloud provider?"
- "Break down assets by criticality and workspace"
- "Show assets discovered in the last 30 days"
- "List credentialed vs non-credentialed scan coverage"
- "Which assets are exposed-to-internet AND have open critical exposures?" (compound filter)
Pre-flight
Step 0 — Account preflight (CC-1)
See _shared/account-preflight.md. Resolve the account-id(s), validate access, and hold them for the rest of the turn. If the question implies a workspace subset ("prod", "EU BU"), also resolve workspace-ids via getEffectiveAccessWorkspaces.
Before using this skill, read every file in the references folder, including the shared references/_shared/ docs. Prefer the cached field catalogs (source-fields.md for source mode, composite-fields.md for composite mode) over calling getApiFields — only fall back to the live tool when an entity or field is missing from the cache.
Step 0.5 — Detect composite vs source data model (critical)
See _shared/composite-vs-source.md. Call getAccountSettings(account-id, settings: ["COMPOSITE_ASSET_LIST_VIEW"]) — "true" means composite, anything else means source. This determines:
- Tool to call:
assetQuery(composite) orsearchAssetData(source). - Field prefix:
compositeAsset.*vsasset.*.
Cache the flag for the turn. Picking the wrong model returns empty results with no error.
Suggested tools
Primary (pick one pair based on data model)
Composite accounts:
assetQuery— flat list and single-field bucketed count
Source accounts:
searchAssetData— flat listaggregateAssetData— single-field bucketed count
There is no compound-filter + aggregation tool. For "filtered list and bucket counts", run the search and the aggregate as two sequential calls and combine client-side.
Supporting
getApiFieldswithentityType: ["ASSET"]— field discovery when unsure of the field namegetGroupByFields— valid aggregation dimensionsgetTopValues— enum-like value discovery for a field (buildin [...]sets)getAccountSettings/getAccountPreferences— composite FF detectiongetEffectiveAccessWorkspaces/getWorkspacesByAccountId— workspace scopingvalidateFilter— FQL syntax validation
Deep links (CC-2)
createDeepLink(preferred) — build a URL from entity type + filter (call once per list/aggregation, plus once per bucket if you need per-bucket links)getDeepLink— URL for a known assetId
See _shared/deep-links.md.
Workflow
Step 1 — Understand the ask
Classify the question into one of:
| Shape | Example | Tool |
|---|---|---|
| Flat list | "Show me all assets where X" | search*Data / assetQuery |
| Bucketed count (one group-by) | "How many assets by type?" | aggregate*Data /assetQuery |
| Compound filter + aggregation | "Exposed-to-internet AND critical, grouped by workspace" | search*Data and aggregate*Data (two calls; combine client-side) or assetQuery in case of composite mode |
| Aggregation + deep links per bucket | "Bucketed counts I can click into" | createDeepLink |
Step 2 — Discover fields if uncertain
If you're unsure of a field path or acceptable value:
getApiFields(entityType=["ASSET"])
getTopValues(field="asset.mappedAttributes.cloudProperties.provider") # or compositeAsset equivalent
getGroupByFields(entityType="ASSET")
Step 3 — Compose FQL
Follow _shared/fql-grammar.md. Critical rules:
- Use the correct prefix (
asset.*orcompositeAsset.*) based on Step 0.5. - Compound filters use parentheses and explicit
AND/OR. - Bare field names on LHS, single-quoted string values on RHS.
asset.criticalityis numeric — compare with>=,=,<using integers (e.g.,asset.criticality >= 4).
Optional sanity check: validateFilter before firing the actual query.
Step 4 — Pick and call the right tool
Apply the Step 1 classification. For compound filters + aggregation, fire search*Data and aggregate*Data with the same filters string — the search returns the row list, the aggregate returns the bucket counts.
Use *Query in case of composite mode.
Step 5 — Deep link every result (CC-2)
- For each list: one
createDeepLinkfor the filtered Assets view. - For aggregations: one
createDeepLinkper bucket, with the bucket's value narrowed into the filter (e.g. addAND <field> = '<bucket-value>'). - For a single asset drill-down:
getDeepLink(assetId).
Step 6 — Emit response
**Query:** <plain-English restatement>
**Account:** <account name + id> (composite / source)
**Filter:** `<FQL>`
<table of results with a "View in Platform" column>
[View full list in Platform](<top-level deep link>)
Tool-selection cheat sheet
| Filter shape | No aggs | With aggs |
|---|---|---|
asset.status = 'active' (single) |
searchAssetData |
aggregateAssetData |
asset.reachability = 'Exposed' AND asset.criticality >= 4 (compound, numeric criticality) |
searchAssetData |
searchAssetData + aggregateAssetData (two calls, same filter) |
For composite accounts there's no separate searchCompositeAssetData / aggregateCompositeAssetData — use the single assetQuery tool with compositeAsset.* prefixes; it returns the row list and bucket counts in one call.
Common recipes
"Asset distribution by business unit / workspace"
# Source mode
aggregateAssetData
aggs: [{ name: "byWorkspace", function: { type: "TERMS", field: "asset.workspaces.name", size: 25 } }]
# Composite mode
assetQuery
aggs: [{ name: "byWorkspace", function: { type: "TERMS", field: "compositeAsset.workspaces.name", size: 25 } }]
Enrich workspace-ids → names via getWorkspacesByAccountId.
"Exposed-to-internet prod assets with open critical exposures"
There are no asset.exposure.* rollup fields — the asset record does not carry exposure counts/severity. Run a two-step pattern: query EXPOSURE first, then ASSET (use composite variants if the account is composite):
# 1) Find exposures matching the criteria, collect assetIds
searchExposureData
filters: exposure.status = 'Open'
AND exposure.scores.scoreLevel = 'Critical'
fields: ["exposure.assetId"]
# 2) Pull the asset records for those ids, scoped to prod workspaces and exposed-to-internet
# NOTE: FQL list literals use parentheses, not square brackets.
searchAssetData
filters: asset.assetId in ('<id1>','<id2>',...)
AND asset.reachability = 'Exposed'
AND asset.workspaces.id in (<prod-ws-id-1>, <prod-ws-id-2>)
# 3) Bucket counts on the same asset filter (e.g., by asset type).
# aggs entries are {name, function: {type, field, size?}}.
# `function` is an object with a `type` discriminator (TERMS for
# bucketing, COUNT/SUM/MIN/MAX/AVG for metrics, DATE_HISTOGRAM for
# time series). String-form `function: "TERMS"` is rejected.
aggregateAssetData
filters: <same as step 2>
aggs: [{ name: "by_type", function: { type: "TERMS", field: "asset.assetType", size: 25 } }]
"Assets discovered in last 30 days"
searchAssetData
filter: "asset.firstDiscoveredOn" >= "<ISO-8601 30 days ago>"
sort: "asset.firstDiscoveredOn:desc"
"Credentialed vs non-credentialed scan coverage"
aggregateAssetData
aggs: [{
name: "byCredentialed",
function: { type: "TERMS", field: "asset.mappedAttributes.isCredentialed", size: 2 }
}]
# or asset.isCredentialedAsset — confirm the active field via getApiFields
Scope guard (CC-3)
- If the user asks about threat actors, ransomware, or CVE-level intel → hand off to
securin-cve-enrichmentorsecurin-threat-correlation. - If the user asks about specific exposure records (as opposed to assets with exposures) → hand off to
securin-exposure-triage. - If no skill fits and the user needs a capability we don't cover → fall back to the platform's built-in
Securin__search_toolsmeta-tool to find the right MCP tool by description.
Edge cases
- Zero results — double-check the composite/source routing (most common cause) before reporting empty.
- Very large result sets — paginate. Ask the user if they want aggregate instead of listing.
- Field name ambiguity — always confirm via
getApiFields. Don't guess field paths across accounts; custom attributes differ. - Workspace access mismatch — if the user asks about a workspace they can't see,
getEffectiveAccessWorkspaceswill omit it; surface the gap.
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
- Asset Fields Quickref — common asset fields + aggregation dimensions
- Shared: Account Preflight
- Shared: Composite vs Source
- Shared: Deep Links
- Shared: FQL Grammar
- Shared: Sorting Rules
- Shared: Brand & Visual Communication