# GreyNoise API Spectral Ruleset # Enforces conventions observed across the GreyNoise v3 / Enterprise APIs. extends: spectral:oas rules: # ── INFO / METADATA ───────────────────────────────────────────────── greynoise-info-title-prefix: description: API title must begin with "GreyNoise". message: '{{description}} — title "{{value}}" should start with "GreyNoise".' severity: error given: "$.info.title" then: function: pattern functionOptions: match: "^GreyNoise" greynoise-info-version-semver: description: API version must be a semver string (e.g. 3.0.0). severity: error given: "$.info.version" then: function: pattern functionOptions: match: "^\\d+\\.\\d+\\.\\d+$" greynoise-info-description-required: description: Info description is required and must be at least 40 characters. severity: error given: "$.info" then: field: description function: length functionOptions: min: 40 greynoise-info-contact-required: description: A contact email is required (typically hello@greynoise.io). severity: warn given: "$.info.contact" then: field: email function: truthy greynoise-info-license-required: description: A license entry is required. severity: warn given: "$.info" then: field: license function: truthy # ── OPENAPI VERSION ───────────────────────────────────────────────── greynoise-openapi-version: description: OpenAPI version must be 3.0.x to match GreyNoise published specs. severity: error given: "$.openapi" then: function: pattern functionOptions: match: "^3\\.0\\." # ── SERVERS ───────────────────────────────────────────────────────── greynoise-servers-defined: description: At least one server entry must be defined. severity: error given: "$.servers" then: function: schema functionOptions: schema: type: array minItems: 1 greynoise-servers-https: description: All server URLs must use HTTPS. severity: error given: "$.servers[*].url" then: function: pattern functionOptions: match: "^https://" greynoise-servers-canonical-host: description: Primary server URL should be https://api.greynoise.io. severity: warn given: "$.servers[0].url" then: function: pattern functionOptions: match: "^https://api\\.greynoise\\.io" # ── PATHS — NAMING CONVENTIONS ────────────────────────────────────── greynoise-paths-lowercase: description: Paths must be all lowercase. severity: error given: "$.paths.*~" then: function: pattern functionOptions: match: "^[^A-Z]*$" greynoise-paths-no-trailing-slash: description: Paths must not end with a trailing slash. severity: error given: "$.paths.*~" then: function: pattern functionOptions: notMatch: ".+/$" greynoise-paths-versioned: description: Paths should begin with /v1, /v2, /v3, or /ping (utility). severity: warn given: "$.paths.*~" then: function: pattern functionOptions: match: "^/(v[0-9]+/|ping$)" # ── OPERATIONS ────────────────────────────────────────────────────── greynoise-operation-summary-required: description: Every operation must have a summary in Title Case. severity: error given: "$.paths.*[get,post,put,patch,delete]" then: field: summary function: truthy greynoise-operation-description-required: description: Every operation must have a description. severity: warn given: "$.paths.*[get,post,put,patch,delete]" then: field: description function: truthy greynoise-operation-operationid-required: description: Every operation must declare an operationId. severity: error given: "$.paths.*[get,post,put,patch,delete]" then: field: operationId function: truthy greynoise-operation-tags-required: description: Every operation must declare at least one tag. severity: warn given: "$.paths.*[get,post,put,patch,delete]" then: field: tags function: schema functionOptions: schema: type: array minItems: 1 # ── TAGS ──────────────────────────────────────────────────────────── greynoise-tags-defined-globally: description: A global tags array should be defined. severity: warn given: "$" then: field: tags function: truthy greynoise-tag-title-case: description: Tag names should be in Title Case (e.g. "IP Lookup", "GNQL"). severity: warn given: "$.tags[*].name" then: function: pattern functionOptions: match: "^[A-Z][A-Za-z0-9 ]+$" # ── PARAMETERS ────────────────────────────────────────────────────── greynoise-parameter-description: description: Every parameter must have a description. severity: warn given: "$.paths.*[get,post,put,patch,delete].parameters[*]" then: field: description function: truthy greynoise-parameter-name-snake-case: description: Parameter names should be snake_case (GreyNoise convention). severity: warn given: "$.paths.*[get,post,put,patch,delete].parameters[*].name" then: function: pattern functionOptions: match: "^[a-z][a-z0-9_]*$" greynoise-api-key-in-header: description: API key must be passed via header, never query string. severity: error given: "$.paths.*[get,post,put,patch,delete].parameters[?(@.name=='key' || @.name=='api_key' || @.name=='apikey')]" then: field: in function: pattern functionOptions: match: "^header$" # ── REQUEST BODIES ────────────────────────────────────────────────── greynoise-request-body-json: description: Request bodies must include application/json. severity: warn given: "$.paths.*[post,put,patch].requestBody.content" then: field: application/json function: truthy # ── RESPONSES ─────────────────────────────────────────────────────── greynoise-response-2xx-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\\d\\d$": type: object minProperties: 1 greynoise-response-401-recommended: description: Authenticated operations should declare a 401 response. severity: info given: "$.paths.*[get,post,put,patch,delete].responses" then: field: "401" function: truthy greynoise-response-429-recommended: description: Operations should declare a 429 (rate-limit) response. severity: info given: "$.paths.*[get,post,put,patch,delete].responses" then: field: "429" function: truthy greynoise-response-description-required: description: Every response must have a description. severity: warn given: "$.paths.*[get,post,put,patch,delete].responses.*" then: field: description function: truthy # ── SCHEMAS — PROPERTY NAMING ─────────────────────────────────────── greynoise-schema-property-snake-case: description: Schema property names should be snake_case (GreyNoise convention). severity: warn given: "$.components.schemas[*].properties.*~" then: function: pattern functionOptions: match: "^[a-z][a-z0-9_]*$" greynoise-schema-type-required: description: Schemas should declare a type (unless using oneOf/allOf/anyOf/$ref). severity: warn given: "$.components.schemas[?(!@.oneOf && !@.allOf && !@.anyOf && !@.$ref)]" then: field: type function: truthy # ── SECURITY ──────────────────────────────────────────────────────── greynoise-security-defined-globally: description: A global security entry must be defined. severity: error given: "$" then: field: security function: truthy greynoise-security-scheme-apikey-header: description: The APIKeyHeaderAuth scheme must be apiKey/header named "key". severity: error given: "$.components.securitySchemes.APIKeyHeaderAuth" then: function: schema functionOptions: schema: type: object required: [type, in, name] properties: type: {const: apiKey} in: {const: header} name: {const: key} # ── HTTP METHOD CONVENTIONS ───────────────────────────────────────── greynoise-get-no-request-body: description: GET operations must not declare a requestBody. severity: error given: "$.paths.*.get" then: field: requestBody function: falsy greynoise-delete-no-request-body: description: DELETE operations must not declare a requestBody. severity: warn given: "$.paths.*.delete" then: field: requestBody function: falsy # ── GENERAL QUALITY ───────────────────────────────────────────────── greynoise-operation-examples-encouraged: description: Operations are encouraged to provide response examples for Microcks mocking. severity: info given: "$.paths.*[get,post,put,patch,delete].responses['200'].content.application/json" then: function: truthy