API Reference

Adding evidence

Most of this guide covers reading data, but the API can also write data through mutations. This page walks through a complete example: a custom integration that attaches an evidence file to a control by creating an execution task and adding the file to it.

Warning

Mutations require a token with write permissions. Authenticate with the Device Authorization Grant so the token acts as a user with the Regular User or Super User role. Read-only machine-to-machine credentials cannot run mutations.

How it works

Evidence in Tidal Control is a document linked to an activity. Attaching evidence to a control is four steps:

  1. Find the control you want to add evidence to
  2. Upload the evidence file as a document
  3. Create an execution task linked to the control
  4. Attach the document to the task as evidence

Step 1 — Find the control

Search for the control by name to get its id:

query FindControl($filter: ControlFilter) {
  controls_paged(first: 1, filter: $filter) {
    edges {
      node {
        id
        sequenceId
        name
      }
    }
  }
}

Variables:

{ "filter": { "search": "Access control policy" } }

Step 2 — Upload the evidence file

Documents are uploaded through a REST endpoint (not GraphQL), as multipart/form-data on the files field. Use the same Bearer token:

curl -s -X POST https://portal.tidalcontrol.com/documents \
  -H "Authorization: Bearer {access_token}" \
  -F "files=@evidence.pdf"

The response is an array of the documents you uploaded:

[
  {
    "id": "11111111-1111-1111-1111-111111111111",
    "name": "evidence.pdf",
    "type": "application/pdf"
  }
]

Keep the document id for the next steps.

Info

The maximum upload size is 128 MB per request. You can upload several files in one request by repeating the files field.

Step 3 — Create an execution task on the control

Create an execution task and link it to the control from step 1 using the controls field:

mutation CreateExecution($input: ExecutionInput!) {
  addExecution(input: $input) {
    id
    sequenceId
    name
  }
}

Variables:

{
  "input": {
    "name": "Quarterly access review",
    "controls": ["22222222-2222-2222-2222-222222222222"]
  }
}

Only name is required. controls takes one or more control ids, and you can also set description, expires, owners, and assets. The mutation returns the new task's id.

Step 4 — Attach the evidence

Attach the uploaded document to the task with addEvidenceToExecution:

mutation AddEvidence($activityId: ID!, $documentIds: [ID!]!) {
  addEvidenceToExecution(activityId: $activityId, documentIds: $documentIds) {
    id
    sequenceId
    evidence {
      id
      document {
        name
      }
    }
  }
}

Variables:

{
  "activityId": "33333333-3333-3333-3333-333333333333",
  "documentIds": ["11111111-1111-1111-1111-111111111111"]
}

The evidence now appears on the task, which is linked to your control.

Complete Python example

This script ties the four steps together. It reuses the get_token() and graphql() helpers from Authentication.

import requests

GRAPHQL_URL = "https://portal.tidalcontrol.com/graphql"
DOCUMENTS_URL = "https://portal.tidalcontrol.com/documents"


def find_control(search, access_token):
    query = """
    query FindControl($filter: ControlFilter) {
      controls_paged(first: 1, filter: $filter) {
        edges { node { id sequenceId name } }
      }
    }
    """
    data = graphql(query, {"filter": {"search": search}}, access_token)
    edges = data["controls_paged"]["edges"]
    if not edges:
        raise RuntimeError(f"No control found for '{search}'")
    return edges[0]["node"]


def upload_document(file_path, access_token):
    with open(file_path, "rb") as f:
        r = requests.post(
            DOCUMENTS_URL,
            headers={"Authorization": f"Bearer {access_token}"},
            files={"files": f},
        )
    r.raise_for_status()
    return r.json()[0]  # first uploaded document


def create_execution(name, control_id, access_token):
    mutation = """
    mutation CreateExecution($input: ExecutionInput!) {
      addExecution(input: $input) { id sequenceId name }
    }
    """
    data = graphql(
        mutation,
        {"input": {"name": name, "controls": [control_id]}},
        access_token,
    )
    return data["addExecution"]


def add_evidence(activity_id, document_id, access_token):
    mutation = """
    mutation AddEvidence($activityId: ID!, $documentIds: [ID!]!) {
      addEvidenceToExecution(activityId: $activityId, documentIds: $documentIds) {
        id
        sequenceId
        evidence { id document { name } }
      }
    }
    """
    data = graphql(
        mutation,
        {"activityId": activity_id, "documentIds": [document_id]},
        access_token,
    )
    return data["addEvidenceToExecution"]


if __name__ == "__main__":
    token = get_token()
    access_token = token["access_token"]

    control = find_control("Access control policy", access_token)
    document = upload_document("evidence.pdf", access_token)
    task = create_execution("Quarterly access review", control["id"], access_token)
    result = add_evidence(task["id"], document["id"], access_token)

    print(f"Added '{document['name']}' to task #{result['sequenceId']} on control {control['name']}.")

Attaching evidence to an existing task

If a suitable execution task already exists, skip step 3. Find the task with the activities_paged query (see Exporting tasks), then call addEvidenceToExecution with its id.