diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml new file mode 100644 index 0000000..9d3a2ab --- /dev/null +++ b/.github/workflows/openapi.yml @@ -0,0 +1,82 @@ +name: OpenAPI + +on: [push] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Use Node.js + uses: actions/setup-node@v4 + - name: Setup spectral + run: npm install -g @stoplight/spectral-cli + - name: Install IBM Cloud ruleset + run: npm install @ibm-cloud/openapi-ruleset + - name: Lint OpenAPI spec + run: spectral lint docs/openapi.yaml + + build-html: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v5 + - name: Use Node.js + uses: actions/setup-node@v4 + - name: Setup OpenAPI generator + run: npm install -g @openapitools/openapi-generator-cli + - name: Build HTML documentation + run: openapi-generator-cli generate --generator-name html2 --input-spec docs/openapi.yaml --output dist + - name: Upload HTML documentation as artifact + uses: actions/upload-artifact@v4 + with: + name: openapi-html + path: dist/index.html + + release: + runs-on: ubuntu-latest + needs: build-html + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + steps: + - name: Validate tag format + run: | + TAG="${GITHUB_REF#refs/tags/}" + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Tag $TAG does not match the required format v[0-9]+.[0-9]+.[0-9]+" + exit 1 + fi + API_VERSION="$(sed -n '/^ *version:/ { s/.*version: //; p; q; }' docs/openapi.yaml)" + if [[ "$TAG" != "v$API_VERSION" ]]; then + echo "Tag $TAG does not match the API version $API_VERSION in docs/openapi.yaml" + exit 1 + fi + - name: Checkout code + uses: actions/checkout@v5 + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + files: docs/openapi.yaml + + publish: + runs-on: ubuntu-latest + needs: release + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + actions: read + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + steps: + - name: Download artifact from build-html job + uses: actions/download-artifact@v4 + with: + name: openapi-html + path: public + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./public diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd1a175 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/package.json +/package-lock.json +/node_modules/ +/openapitools.json diff --git a/.spectral.yaml b/.spectral.yaml new file mode 100644 index 0000000..4155b35 --- /dev/null +++ b/.spectral.yaml @@ -0,0 +1,29 @@ +extends: ["spectral:oas", "spectral:asyncapi", "spectral:arazzo", "@ibm-cloud/openapi-ruleset"] +rules: + # Documentation: https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-intro + + # Legacy: the first API version named paths in camelCase. + ibm-path-segment-casing-convention: info + ibm-operationid-casing-convention: info + # Legacy: synchronous check returns a list of findings directly. + ibm-no-array-responses: info + # Legacy: the list of findings in the asynchronous result is optional because it depends on the status. + ibm-required-array-properties-in-response: info + # Legacy: history data can be empty. + ibm-accept-and-return-models: info + + # THOR Finding's properties have various different value types, even on the first level. + ibm-well-defined-dictionaries: off + # Some version information and status details are mistakenly recognized as date-time format strings. + ibm-use-date-based-format: off + # Reported errors do not obey IBM's error container model, cf. https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-errors + ibm-error-response-schemas: off + + # Unfortunately, the ibm-*-attributes bundle useful rules (like `format` for + # integers) with doubtful ones (like `maximum` for integers) which often can + # not be met in general. As those are SHOULD rules anyway, degrade them to + # info level which will, by default, show up in the report but not fail the + # validation. + ibm-array-attributes: info + ibm-integer-attributes: info + ibm-string-attributes: info diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..52848f9 --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,515 @@ +openapi: 3.0.4 +info: + title: THOR Thunderstorm API + description: >- + This API allows you to send files to THOR to scan them and provides + information about the running THOR instance. + version: 1.0.0 +servers: + - url: http://localhost:8080/api/v1 + description: Local THOR instance +tags: + - name: scan + description: Endpoints to scan files with THOR + - name: results + description: Endpoints to retrieve results of asynchronous scans + - name: info + description: Endpoints to retrieve information about the running THOR instance +paths: + /check: + post: + summary: Check a file with THOR + description: Check a file with THOR + tags: + - scan + operationId: check + parameters: + - $ref: "#/components/parameters/SampleSource" + requestBody: + $ref: "#/components/requestBodies/FileUpload" + responses: + "200": + description: Returns a list of findings + content: + application/json: + schema: + $ref: "#/components/schemas/ThorReport" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + /checkAsync: + post: + summary: Check a file with THOR asynchronously + description: Check a file with THOR asynchronously + tags: + - scan + operationId: checkAsync + parameters: + - $ref: "#/components/parameters/SampleSource" + requestBody: + $ref: "#/components/requestBodies/FileUpload" + responses: + "200": + description: Returns a map containing the sample ID + content: + application/json: + schema: + $ref: "#/components/schemas/SampleId" + example: + id: 12345 + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + /getAsyncResults: + get: + summary: Retrieve the results of an asynchronous file check + description: Retrieve the results of an asynchronous file check. + tags: + - results + operationId: getAsyncResults + parameters: + - description: Sample ID + name: id + in: query + required: true + schema: + $ref: "#/components/schemas/SampleId/properties/id" + responses: + "200": + description: >- + Returns a JSON with the current status and, if applicable, the + results + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncResult" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + /queueHistory: + get: + summary: Retrieve a history of how many asynchronous requests were queued + description: Retrieve a history of how many asynchronous requests were queued + tags: + - info + operationId: queueHistory + parameters: + - $ref: "#/components/parameters/AggregationPeriod" + - $ref: "#/components/parameters/HistoryLimit" + responses: + "200": + $ref: "#/components/responses/QueueHistory" + "400": + $ref: "#/components/responses/BadRequest" + /sampleHistory: + get: + summary: Retrieve a history of how many samples were scanned + description: Retrieve a history of how many samples were scanned. + tags: + - info + operationId: sampleHistory + parameters: + - $ref: "#/components/parameters/AggregationPeriod" + - $ref: "#/components/parameters/HistoryLimit" + responses: + "200": + $ref: "#/components/responses/SampleHistory" + "400": + $ref: "#/components/responses/BadRequest" + /info: + get: + summary: Receive static information about the running THOR instance + description: Receive static information about the running THOR instance. + tags: + - info + operationId: info + responses: + "200": + description: Map with values to running version, rate limitation, ... + content: + application/json: + schema: + $ref: "#/components/schemas/ThunderstormInfo" + "500": + $ref: "#/components/responses/InternalServerError" + /status: + get: + summary: Receive live information about the running THOR instance + description: Receive live information about the running THOR instance. + tags: + - info + operationId: status + responses: + "200": + description: Map with values to scan times, scanned samples, wait times, ... + content: + application/json: + schema: + $ref: "#/components/schemas/ThunderstormStatus" +components: + schemas: + FileObject: + description: Wrapped file + type: object + properties: + file: + description: File to be checked + type: string + format: binary + required: + - file + ThorReport: + description: THOR Report containing findings + type: array + items: + $ref: "#/components/schemas/ThorFinding" + example: + - type: THOR finding + meta: + time: '2026-01-16T13:35:34.11133172+01:00' + level: Alert + module: HTTPServer + scan_id: S-qkQv5yHIUHk-2 + hostname: 127.0.0.1 + message: Malicious file found + subject: + type: file + path: somefile.exe + exists: 'yes' + extension: .exe + magic_header: EXE + hashes: + md5: 7168892693d7716220d98883fffd848c + sha1: 42acd9b554e1c843a9f8139022b560de0bf48682 + sha256: 660464c473c47784d8820d3e268c0d1327ac22ce0e607dc35e858628c53f0687 + first_bytes: + hex: 4d5a90000300000004000000ffff0000b8000000 + ascii: MZ + size: 1352192 + permissions: null + content: + type: sparse data + elements: + - offset: 856110 + data: '>`ncrypt.dll' + length: 1352192 + score: 94 + reasons: + - type: reason + summary: some YARA rule + signature: + score: 85 + reference: + - Internal Research + origin: internal + kind: YARA Rule + date: '2023-05-12' + tags: + - EXE + - HKTL + rule_name: Some_Rule_Name + description: Detects Something + matched: + - data: '%*s**CREDENTIAL**' + offset: 918480 + field: /content + - data: '%*s Persist : %08x - %u - %s' + offset: 919056 + field: /content + - data: '%*s**DOMAINKEY**' + offset: 950688 + field: /content + - type: reason + summary: Another rule + signature: + score: 80 + reference: + - Some reference + origin: internal + kind: YARA Rule + date: '2016-02-05' + tags: + - T1059_001 + rule_name: Another_Rule_Name + description: Detects Something other + matched: + - data: kuhl_m_lsadump_getUsersAndSamKey ; kull_m_registry_RegOpenKeyEx SAM + Accounts (0x%08x) + offset: 1100540 + field: /content + - data: kuhl_m_lsadump_getComputerAndSyskey ; kuhl_m_lsadump_getSyskey KO + offset: 1099708 + field: /content + reason_count: 28 + context: null + log_version: v3.0.0 + ThorFinding: + description: THOR Finding + type: object + additionalProperties: true # Any type + SampleId: + description: Sample ID returned for an asynchronous scan request + type: object + properties: + id: + description: Sample ID + type: integer + format: int64 + required: + - id + example: + id: 12345 + AsyncResult: + description: Result object for asynchronous scan operations + type: object + properties: + status: + description: Current status of the scan + type: string + # TODO: Use enum? (would require snake-cased keywords) + # values: ["Waiting for execution", "Currently being scanned", "Sample analysis complete", "Sample analysis failed", "invalid"] + example: + Currently being scanned + result: + $ref: "#/components/schemas/ThorReport" + required: + - status + example: + result: + - type: THOR finding + meta: + time: '2026-01-19T16:08:27.809483955+01:00' + level: Warning + module: HTTPServer + scan_id: S-qiVIo7fm2Yk-4 + hostname: 127.0.0.1 + message: Suspicious file found + subject: + type: file + path: 6700758b14fe8ac1bbcbbf3d652613d8 + exists: 'yes' + extension: '' + magic_header: UNKNOWN + hashes: + md5: ffc74d5afc22d1f1c785f98154cc7bae + sha1: 9603a4806528578686dc5f02b805c64414b3c91b + sha256: da1d2378fbacf84d09d18a94def83cd3bfc06e89e7429ba3de57cd96a0e04a87 + first_bytes: + hex: 2f2a0ae8aeade8aeb0200a5b726577726974655f + ascii: /* [rewrite_ + size: 5369 + permissions: null + content: + type: sparse data + elements: + - offset: 273 + data: var _0x + length: 5369 + score: 79 + reasons: + - type: reason + summary: YARA rule OBFUSC + signature: + score: 65 + origin: internal + kind: YARA Rule + date: '2025-03-29' + tags: + - OBFUS + rule_name: OBFUSC + description: Detects obfuscated JavaScript code + matched: + - data: ''']=!![];}' + offset: 3865 + field: /content + reason_count: 1 + context: null + log_version: v3.0.0 + status: Sample analysis complete + TimestampMap: + description: Map of timestamps to integer values + type: object + additionalProperties: + type: integer + format: int64 + example: + 2026-01-21 06:45: 0 + 2026-01-21 07:00: 9165 + 2026-01-21 07:15: 49023 + 2026-01-21 07:30: 61685 + 2026-01-21 07:45: 54932 + ThunderstormInfo: + description: Information about the Thunderstorm service instance, including version details, configuration, and license information. + type: object + properties: + version_info: + $ref: "#/components/schemas/ThunderstormVersionInfo" + arguments: + description: Command-line arguments used to start the underlying THOR instance. + type: array + items: + type: string + example: ["--config", "/opt/nextron/thunderstorm/config/custom-thor.yml"] + license_expiration: + description: Expiration date and time of the license in ISO 8601 format. + type: string + format: date-time + example: "2026-07-08T00:00:00Z" + license_owner: + description: Name of the license owner. + type: string + example: "admin" + scan_speed_limitation: + description: Scan speed limitation in bytes per second. A value of 0 indicates no limitation. + type: integer + format: int64 + example: 0 + threads: + description: Number of threads used for scanning. + type: integer + format: int64 + example: 16 + required: + - version_info + - arguments + - license_expiration + - license_owner + - scan_speed_limitation + - threads + example: + version_info: + thor: "10.8.0" + build: "2026-01-07 04:02:31" + signatures: "2026/01/12-160550" + sigma: "r2025-12-01-18-g6d581764e" + arguments: + - "--config" + - "/opt/nextron/thunderstorm/config/custom-thor.yml" + license_expiration: "2026-07-08T00:00:00Z" + license_owner: "admin" + scan_speed_limitation: 0 + threads: 16 + ThunderstormVersionInfo: + description: Version information for various components of the Thunderstorm service. + type: object + properties: + thor: + description: Version of the THOR scanner engine. + type: string + example: "10.8.0" + build: + description: Build timestamp of the THOR scanner. + type: string + example: "2026-01-07 04:02:31" + signatures: + description: Version/timestamp of the signature database. + type: string + example: "2026/01/12-160550" + sigma: + description: Version identifier of the Sigma rules. + type: string + example: "r2025-12-01-18-g6d581764e" + ThunderstormStatus: + description: Live information about the running THOR instance + type: object + properties: + scanned_samples: + description: Total number of samples that have been scanned + type: integer + format: int64 + example: 18686305 + queued_async_requests: + description: Number of asynchronous requests currently in queue + type: integer + format: int64 + example: 0 + avg_scan_time_milliseconds: + description: Average time in milliseconds to scan a sample + type: integer + format: int64 + example: 21 + avg_wait_time_milliseconds: + description: Average wait time in milliseconds before processing + type: integer + format: int64 + example: 0 + required: + - scanned_samples + - queued_async_requests + - avg_scan_time_milliseconds + - avg_wait_time_milliseconds + example: + scanned_samples: 18686305 + queued_async_requests: 0 + avg_scan_time_milliseconds: 21 + avg_wait_time_milliseconds: 0 + Error: + description: Error with message + type: object + properties: + message: + description: Error message describing the problem + type: string + required: + - message + parameters: + SampleSource: + description: Specify source for the THOR log + name: source + in: query + schema: + type: string + AggregationPeriod: + description: Aggregate this many minutes per value (default 1). + name: aggregate + in: query + schema: + type: integer + format: int64 + HistoryLimit: + description: Give a history for the last this many minutes (default infinite). + name: limit + in: query + schema: + type: integer + format: int64 + requestBodies: + FileUpload: + required: true + description: Multipart form data containing a file + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/FileObject" + responses: + QueueHistory: + description: >- + A JSON Map with each time mapped to the queue length + (estimated) from the last time mentioned to that time. + content: + application/json: + schema: + $ref: "#/components/schemas/TimestampMap" + SampleHistory: + description: >- + Returns a JSON Map with each time mapped to the samples scanned from + the last time mentioned to that time + content: + application/json: + schema: + $ref: "#/components/schemas/TimestampMap" + BadRequest: + description: Invalid parameters given + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + InternalServerError: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error"