From 08622df7ed0b14c6bde79569ebb238da4caee0b4 Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Tue, 10 Feb 2026 19:18:45 +0100 Subject: [PATCH] mvp --- README.md | 28 ++++++++ openapi.json | 131 ++++++++++++++++++++++++++++++++++++ tas-upload-sarif/README.md | 48 +++++++++++++ tas-upload-sarif/action.yml | 66 ++++++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 openapi.json create mode 100644 tas-upload-sarif/README.md create mode 100644 tas-upload-sarif/action.yml diff --git a/README.md b/README.md index 754d807..b482ac2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # tas-actions +Reusable GitHub Actions for [TAS (Tea Advanced Security)](https://github.com/go-gitea/gitea): upload SARIF reports and gate CI on the API response. + +## Actions + +### [tas-upload-sarif](tas-upload-sarif/) + +Uploads a SARIF report from a file to TAS and **fails the job** if the API returns `allowed: false`. + +**Example workflow** (e.g. after a security scan that produces SARIF): + +```yaml +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Run your scanner and produce SARIF (e.g. to results.sarif) + # - run: ./run-scanner --output results.sarif + + - name: Upload SARIF to TAS and gate + uses: your-org/tas-actions/tas-upload-sarif@v1 + with: + tas-base-url: 'https://tas.example.com' + sarif-file: 'results.sarif' +``` + +See [tas-upload-sarif/README.md](tas-upload-sarif/README.md) for all inputs and options. \ No newline at end of file diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..cbcf7ec --- /dev/null +++ b/openapi.json @@ -0,0 +1,131 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "TAS — Tea Advanced Security", + "description": "Security reporting and gating API for Gitea. Upload SARIF reports per repo/branch; get allowed/new_findings for CI gating.", + "version": "1.0.0" + }, + "servers": [{ "url": "http://localhost:3000", "description": "Local" }], + "paths": { + "/repos/{owner}/{repo}/branches/{branch}/reports": { + "post": { + "operationId": "postReposByOwnerByRepoBranchesByBranchReports", + "description": "Upload a SARIF report for a repository and branch. Replaces any existing report. On non-default branches, compares to baseline and returns allowed/new_findings for CI gating.", + "request": { + "params": { + "owner": { + "description": "Repository owner (e.g. t.behrendt)", + "required": true + }, + "repo": { + "description": "Repository name (e.g. k_gitea)", + "required": true + }, + "branch": { "description": "Branch name", "required": true } + }, + "body": { + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "SARIF 2.1 document with runs[]" + } + } + } + } + }, + "responses": { + "200": { + "description": "Report stored and (for non-default branch) gating result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allowed": { "type": "boolean" }, + "reason": { "type": "string" }, + "new_critical": { "type": "number" }, + "new_high": { "type": "number" }, + "new_medium": { "type": "number" }, + "new_low": { "type": "number" }, + "new_findings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rule_id": { "type": "string" }, + "level": { "type": "string" }, + "message": { "type": "string" }, + "severity": { + "type": "string", + "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW"] + }, + "artifact_uri": { "type": "string" }, + "region": { + "type": "object", + "properties": { + "start_line": { + "anyOf": [ + { "type": "number" }, + { "type": "null" } + ] + }, + "start_column": { + "anyOf": [ + { "type": "number" }, + { "type": "null" } + ] + } + }, + "required": ["start_line", "start_column"] + } + }, + "required": [ + "rule_id", + "level", + "message", + "severity", + "artifact_uri", + "region" + ] + } + } + }, + "required": [ + "allowed", + "new_critical", + "new_high", + "new_medium", + "new_low", + "new_findings" + ] + } + } + } + } + }, + "parameters": [ + { + "schema": { "type": "string" }, + "in": "path", + "name": "owner", + "required": true + }, + { + "schema": { "type": "string" }, + "in": "path", + "name": "repo", + "required": true + }, + { + "schema": { "type": "string" }, + "in": "path", + "name": "branch", + "required": true + } + ] + } + } + }, + "components": {} +} diff --git a/tas-upload-sarif/README.md b/tas-upload-sarif/README.md new file mode 100644 index 0000000..51e627a --- /dev/null +++ b/tas-upload-sarif/README.md @@ -0,0 +1,48 @@ +# TAS Upload SARIF + +Reusable GitHub Action that uploads a SARIF report to [TAS (Tea Advanced Security)](https://github.com/go-gitea/gitea) and **fails the job** if the API responds with `allowed: false` (e.g. new findings above your policy). + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| `tas-base-url` | Yes | Base URL of the TAS API (e.g. `https://tas.example.com`) | +| `sarif-file` | Yes | Path to the SARIF report file (JSON) | +| `owner` | No | Repository owner (default: `github.repository_owner`) | +| `repo` | No | Repository name (default: `github.event.repository.name`) | +| `branch` | No | Branch name (default: `github.ref_name`) | + +## Usage + +```yaml +- name: Upload SARIF to TAS and gate + uses: your-org/tas-actions/tas-upload-sarif@v1 + with: + tas-base-url: 'https://tas.example.com' + sarif-file: 'results.sarif' +``` + +With explicit owner/repo/branch (e.g. for monorepos or custom refs): + +```yaml +- uses: your-org/tas-actions/tas-upload-sarif@v1 + with: + tas-base-url: ${{ vars.TAS_BASE_URL }} + sarif-file: 'scan-output.sarif' + owner: my-org + repo: my-repo + branch: ${{ github.head_ref }} +``` + +## Behavior + +1. **POST**s the SARIF file to `{tas-base-url}/repos/{owner}/{repo}/branches/{branch}/reports`. +2. On HTTP non-200, the step fails and prints the response body. +3. On success, reads the JSON response: + - If `allowed` is `true`, the step succeeds. + - If `allowed` is `false`, the step **fails** and prints `reason` and the full response (e.g. `new_critical`, `new_high`, `new_findings`). + +## Requirements + +- **GitHub-hosted runners**: `curl` and `jq` are available by default. +- **Self-hosted runners**: ensure `curl` and `jq` are installed. diff --git a/tas-upload-sarif/action.yml b/tas-upload-sarif/action.yml new file mode 100644 index 0000000..53e7362 --- /dev/null +++ b/tas-upload-sarif/action.yml @@ -0,0 +1,66 @@ +name: 'TAS Upload SARIF' +description: 'Upload a SARIF report to TAS (Tea Advanced Security) and fail the job if gating returns allowed: false' +inputs: + tas-base-url: + description: 'Base URL of the TAS API (e.g. https://tas.example.com)' + required: true + sarif-file: + description: 'Path to the SARIF report file (JSON)' + required: true + owner: + description: 'Repository owner (default: GitHub repository owner)' + required: false + repo: + description: 'Repository name (default: GitHub repository name)' + required: false + branch: + description: 'Branch name (default: current ref name, e.g. main)' + required: false +runs: + using: 'composite' + steps: + - name: Upload SARIF to TAS and gate + shell: bash + env: + OWNER: ${{ inputs.owner || github.repository_owner }} + REPO: ${{ inputs.repo || github.event.repository.name }} + BRANCH: ${{ inputs.branch || github.ref_name }} + BASE_URL: ${{ inputs.tas-base-url }} + SARIF_FILE: ${{ inputs.sarif-file }} + run: | + BASE_URL="${BASE_URL%/}" + URL="${BASE_URL}/repos/${OWNER}/${REPO}/branches/${BRANCH}/reports" + echo "Uploading SARIF to TAS: $URL" + + if [[ ! -f "$SARIF_FILE" ]]; then + echo "::error::SARIF file not found: $SARIF_FILE" + exit 1 + fi + + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$URL" \ + -H "Content-Type: application/json" \ + -d @"$SARIF_FILE") + + HTTP_BODY=$(echo "$RESPONSE" | head -n -1) + HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) + + if [[ "$HTTP_CODE" != "200" ]]; then + echo "::error::TAS API returned HTTP $HTTP_CODE" + echo "$HTTP_BODY" | head -20 + exit 1 + fi + + ALLOWED=$(echo "$HTTP_BODY" | jq -r '.allowed') + REASON=$(echo "$HTTP_BODY" | jq -r '.reason // empty') + + if [[ "$ALLOWED" != "true" ]]; then + echo "::error::TAS gating failed (allowed: false). $REASON" + echo "::error::new_critical/new_high/new_medium/new_low are in the API response." + echo "$HTTP_BODY" | jq '.' + exit 1 + fi + + echo "TAS gating passed (allowed: true)." + if [[ -n "$REASON" ]]; then + echo "$REASON" + fi