# VirusTotal API v3 — Spectral ruleset
#
# Enforces the conventions observed across the VirusTotal / Google Threat Intelligence
# API v3 specs: snake_case paths, x-apikey header auth, "data" envelope responses,
# tag prefixes (`IoC Investigation - ...`, `YARA Hunting - ...`), and the JSON:API-ish
# object shape (id + type + attributes + relationships).
#
# Usage:
# spectral lint --ruleset rules/virustotal-rules.yml openapi/*.yml
extends:
- spectral:oas
rules:
# ============================================================
# INFO / METADATA
# ============================================================
vt-info-title-required:
description: Spec title must be present and start with "VirusTotal".
message: "{{property}} must start with 'VirusTotal'"
severity: error
given: $.info.title
then:
function: pattern
functionOptions:
match: "^VirusTotal\\b"
vt-info-version-required:
description: Spec version must be declared and start with "3".
message: "info.version must start with '3' (VirusTotal API v3)."
severity: error
given: $.info.version
then:
function: pattern
functionOptions:
match: "^3"
vt-info-description-required:
description: Spec description must be present and non-trivial.
message: "info.description must be a substantive paragraph (min 40 chars)."
severity: warn
given: $.info.description
then:
function: length
functionOptions:
min: 40
vt-info-contact-required:
description: Spec must declare a contact (VirusTotal / GTI).
severity: warn
given: $.info.contact
then:
function: truthy
vt-info-license-required:
description: Spec must declare a license (VirusTotal Terms of Service).
severity: warn
given: $.info.license
then:
function: truthy
# ============================================================
# OPENAPI VERSION + SERVERS
# ============================================================
vt-openapi-version:
description: Spec must be OpenAPI 3.x.
severity: error
given: $.openapi
then:
function: pattern
functionOptions:
match: "^3\\."
vt-servers-required:
description: At least one server URL must be defined.
severity: error
given: $.servers
then:
function: length
functionOptions:
min: 1
vt-servers-https:
description: Server URL must use HTTPS.
severity: error
given: $.servers[*].url
then:
function: pattern
functionOptions:
match: "^https://"
vt-servers-api-v3-host:
description: Server URL should point at api/v3 on virustotal.com or gtidocs subdomain.
severity: warn
given: $.servers[*].url
then:
function: pattern
functionOptions:
match: "/api/v3$"
# ============================================================
# PATHS — NAMING CONVENTIONS
# ============================================================
vt-paths-snake-case:
description: Path segments use snake_case (VirusTotal convention) — letters, digits, underscores, hyphens, and {param} placeholders only.
message: "Path '{{value}}' must use snake_case segments."
severity: warn
given: $.paths.*~
then:
function: pattern
functionOptions:
match: "^(/[a-z0-9_]+(-[a-z0-9_]+)*|/\\{[a-zA-Z_]+\\})+$"
vt-paths-no-trailing-slash:
description: Paths must not end with a trailing slash.
severity: error
given: $.paths.*~
then:
function: pattern
functionOptions:
notMatch: "/$"
vt-paths-no-query-string:
description: Path keys must not contain '?' query strings.
severity: error
given: $.paths.*~
then:
function: pattern
functionOptions:
notMatch: "\\?"
# ============================================================
# OPERATIONS
# ============================================================
vt-operation-summary-required:
description: Every operation must have a summary.
severity: error
given: $.paths.*.[get,post,put,patch,delete].summary
then:
function: truthy
vt-operation-summary-virustotal-prefix:
description: Operation summaries must start with "VirusTotal " (per title-case-summaries skill).
message: "Summary should be prefixed with 'VirusTotal '."
severity: warn
given: $.paths.*.[get,post,put,patch,delete].summary
then:
function: pattern
functionOptions:
match: "^VirusTotal "
vt-operation-description-required:
description: Every operation must have a description.
severity: warn
given: $.paths.*.[get,post,put,patch,delete]
then:
field: description
function: truthy
vt-operation-operationid-required:
description: Every operation must have an operationId.
severity: error
given: $.paths.*.[get,post,put,patch,delete]
then:
field: operationId
function: truthy
vt-operation-operationid-camelcase:
description: operationId should be camelCase.
message: "operationId '{{value}}' should be camelCase (no dashes, underscores, or spaces)."
severity: warn
given: $.paths.*.[get,post,put,patch,delete].operationId
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"
vt-operation-tags-required:
description: Every operation must have at least one tag.
severity: error
given: $.paths.*.[get,post,put,patch,delete].tags
then:
function: length
functionOptions:
min: 1
vt-operation-microcks-extension:
description: Operations should declare x-microcks-operation for Microcks mocking.
severity: info
given: $.paths.*.[get,post,put,patch,delete]
then:
field: x-microcks-operation
function: truthy
# ============================================================
# TAGS
# ============================================================
vt-tags-global-defined:
description: Spec must define a global tags array.
severity: warn
given: $.tags
then:
function: length
functionOptions:
min: 1
vt-tags-virustotal-grouping:
description: |
Tag names should follow the VirusTotal grouping convention:
" - " (e.g. "IoC Investigation - Files",
"YARA Hunting - Livehunt", "Access Control - User Management",
"Threat Graphs", "Threat Landscape & Vulnerability Intelligence & Reports & Analysis").
message: "Tag '{{value}}' should follow the ' - ' or '' format used across VirusTotal v3."
severity: info
given: $.tags[*].name
then:
function: pattern
functionOptions:
match: "^(Access Control|IoC Feeds|IoC Investigation|Private Scanning|YARA Hunting|Threat Graphs|Threat Landscape)( - .+| & .+)?$"
# ============================================================
# PARAMETERS
# ============================================================
vt-parameter-description-required:
description: Every parameter must have a description.
severity: warn
given: $.paths.*.[get,post,put,patch,delete].parameters[*]
then:
field: description
function: truthy
vt-parameter-snake-case:
description: Parameter names should be snake_case (VirusTotal convention).
message: "Parameter '{{value}}' should be snake_case."
severity: warn
given: $.paths.*.[get,post,put,patch,delete].parameters[*].name
then:
function: pattern
functionOptions:
match: "^([a-z][a-z0-9_]*|x-[a-z][a-z0-9-]*)$"
vt-parameter-no-apikey-as-query:
description: API key must be sent via the x-apikey header, not as a query parameter.
message: "API key parameter '{{value}}' must be in the x-apikey header, not the query string."
severity: error
given: $.paths.*.[get,post,put,patch,delete].parameters[?(@.in=='query')].name
then:
function: pattern
functionOptions:
notMatch: "^(apikey|api_key|apiKey)$"
vt-parameter-pagination-cursor:
description: List endpoints prefer cursor-based pagination via 'limit' + 'cursor'.
severity: info
given: $.paths.*.get.parameters[?(@.name=='page' || @.name=='offset')]
then:
function: undefined
# ============================================================
# REQUEST BODIES
# ============================================================
vt-requestbody-json-content:
description: Request bodies must offer application/json.
severity: warn
given: $.paths.*.[post,put,patch].requestBody.content
then:
field: application/json
function: truthy
# ============================================================
# RESPONSES
# ============================================================
vt-response-success-required:
description: Every operation must declare at least one 2xx response.
severity: error
given: $.paths.*.[get,post,put,patch,delete].responses
then:
function: schema
functionOptions:
schema:
type: object
patternProperties:
"^2[0-9][0-9]$": {}
required: []
minProperties: 1
vt-response-401-defined:
description: Operations should document the 401 response (missing/invalid API key).
severity: warn
given: $.paths.*.[get,post,put,patch,delete].responses
then:
field: "401"
function: truthy
vt-response-429-defined:
description: Operations should document the 429 response (rate limit / quota exceeded).
severity: warn
given: $.paths.*.[get,post,put,patch,delete].responses
then:
field: "429"
function: truthy
vt-response-json-content:
description: 2xx responses must be served as application/json.
severity: warn
given: $.paths.*.[get,post,put,patch,delete].responses[?(@property.match(/^2\d\d$/))].content
then:
field: application/json
function: truthy
# ============================================================
# SCHEMAS — PROPERTY NAMING
# ============================================================
vt-schema-property-snake-case:
description: Schema property names should be snake_case (VirusTotal convention).
message: "Property '{{property}}' should be snake_case."
severity: warn
given: $.components.schemas.*.properties.*~
then:
function: pattern
functionOptions:
match: "^[a-z][a-z0-9_]*$"
vt-schema-object-shape:
description: |
VirusTotal objects follow a JSON:API-ish shape: { id, type, attributes, relationships }.
Any schema whose name ends with "Object" should declare those four properties.
severity: info
given: "$.components.schemas[?(@property.match(/Object$/))]"
then:
field: properties
function: schema
functionOptions:
schema:
type: object
required: [id, type, attributes]
vt-schema-data-envelope:
description: 'A DataEnvelope schema should be present to model the {"data": ...} response wrapper.'
severity: info
given: $.components.schemas
then:
field: DataEnvelope
function: truthy
vt-schema-error-envelope:
description: 'An ErrorResponse schema should be present to model {"error": {code, message}}.'
severity: info
given: $.components.schemas
then:
field: ErrorResponse
function: truthy
# ============================================================
# SECURITY
# ============================================================
vt-security-global-defined:
description: Spec must declare a global security requirement.
severity: error
given: $.security
then:
function: length
functionOptions:
min: 1
vt-security-scheme-vtapikey:
description: The VTApiKey security scheme (apiKey in header named x-apikey) must be defined.
severity: error
given: $.components.securitySchemes.VTApiKey
then:
function: schema
functionOptions:
schema:
type: object
required: [type, in, name]
properties:
type:
const: apiKey
in:
const: header
name:
const: x-apikey
# ============================================================
# HTTP METHOD CONVENTIONS
# ============================================================
vt-get-no-requestbody:
description: GET operations must not declare a requestBody.
severity: error
given: $.paths.*.get.requestBody
then:
function: falsy
vt-delete-no-requestbody:
description: DELETE operations should not declare a requestBody.
severity: warn
given: $.paths.*.delete.requestBody
then:
function: falsy
# ============================================================
# GENERAL QUALITY
# ============================================================
vt-no-empty-description:
description: Descriptions must not be empty strings.
severity: warn
given: "$..description"
then:
function: truthy
vt-deprecation-documented:
description: Deprecated operations should explain the deprecation in the description.
severity: warn
given: "$.paths.*[?(@.deprecated==true)]"
then:
field: description
function: truthy