Reference

The Pental API: Connect Your Pentest Data to Your Own Tools

The Pental API reads and changes engagements, findings, clients and comments from your own scripts and systems, in the Portal and in our free reporting tool: keys, signing in, every function, and the limits.

9 min read · Updated

The Pental API lets your team connect Pental to the rest of how they work: a tracker that should open a ticket for every high finding, a project board that should show this week's engagements, a dashboard, a script that moves findings into a spreadsheet. It reads engagements, findings, clients, people, comments, timelines and files, and with writing switched on it sets a finding's status, asks for a retest, adds comments, and makes and changes clients and engagements. It is the same in the Portal and in our free reporting tool, and it runs in your own database: Pental's servers are not in the path.

How a key works

A key belongs to one person and works as them. It reads what that person can read in the portal and changes what they can change there, through the API's own functions alone: never a table directly, never a file, never any other function, and never from a web page. A client of yours who could not see a draft finding in the portal cannot see it through a key either, because the same rules in your database decide both.

What it means
Off until turned onAn administrator turns the API on under Settings, API, decides whether keys may write, which roles may make keys (Admin, Senior and Standard to begin with) and the longest a key may live: 90 days to begin with, up to 365.
Made by its personEach person makes their own keys on Profile, under API Keys, choosing a name, whether it reads or reads and writes, and how many days it lives. Making one asks for the second factor again if it was given more than ten minutes ago. Ten keys that work is the most a person can hold.
Shown onceThe key is shown once, as it is made, with your API's address and the publishable key beside it. Only a one-way hash (SHA-256) of it is kept, so it cannot be shown again; if it is lost, make another and revoke the first.
Ends on its ownA key stops working when it expires, when its person or an administrator revokes it, and is revoked on its own when its person is switched off or deleted, changes role, organisation or sign-in, or has their second factor reset. An administrator taking a role off the list pauses its keys until the role is put back, and switching the API off stops every key at once.
Seen by an administratorSettings, API lists every key with its person, what it can do, when it was made, when it expires, when and from where it was last used, how many writes it has made, and its state, with Revoke beside each and Revoke every key for a key that may have got out. The API log records every key made and revoked and every change to the settings, by whom.
Settings, API in our free reporting tool: the API on, writing allowed, the roles that may make keys, the longest life, and every key with its person and state.Corvane SecurityIn our free reporting toolA demonstration: Corvane Security and its client are invented. Every screen is the real software.

Corvane SecurityIn our free reporting toolA demonstration: Corvane Security and its client are invented. Every screen is the real software.Drag sideways to see the whole screen.

Sign in with three calls

A program signs in with the key, then calls the API with the session that gives it. Every call is a POST with a JSON body to your API's address, with the publishable key in the apikey header. All three values are in the dialog that showed the key: Your key, Your API address (your own database's address, or the address you open our free reporting tool at) and The publishable key.

1. rpc_api_key_exchange with p_key, using the publishable key alone, returns the key's person's email and a password made for this sign-in.

2. /auth/v1/token?grant_type=password with that email and password signs in, as every sign-in to the portal ends, and returns an access_token and a refresh_token.

3. rpc_api_key_bind with p_key, sent with the new access token as Authorization: Bearer, makes that session the key's. It has to come within a minute of the exchange, and only the session that exchange signed in can be bound. From then on the session calls the api_v1_ functions.

The access token lasts an hour unless your database's own settings say otherwise. A program that runs for longer renews it with the refresh token (/auth/v1/token?grant_type=refresh_token), which keeps the same session, rather than signing in again for every call: a key keeps its five newest sessions and ends older ones, a key exchanges ten times a minute at most, and on Pental's gateway and in our free reporting tool thirty sign-ins every five minutes is the most from one address. Keep the key out of your code and your command lines: the examples read it, the address and the publishable key from environment variables.

Python

import os
import requests

BASE = os.environ["PENTAL_API_URL"]
ANON = os.environ["PENTAL_PUBLISHABLE_KEY"]
KEY = os.environ["PENTAL_API_KEY"]


def post(path, body, token=None):
    headers = {"apikey": ANON, "Authorization": "Bearer " + (token or ANON)}
    r = requests.post(BASE + path, json=body, headers=headers, timeout=30)
    if not r.ok:
        raise SystemExit(f"{r.status_code} {r.text}")
    return r.json() if r.content else None


# 1. The key for a password made for this sign-in
x = post("/rest/v1/rpc/rpc_api_key_exchange", {"p_key": KEY})
# 2. Sign in with it
session = post("/auth/v1/token?grant_type=password",
               {"email": x["email"], "password": x["password"]})
token = session["access_token"]
# 3. Make the session the key's
post("/rest/v1/rpc/rpc_api_key_bind", {"p_key": KEY}, token)


def api(name, **args):
    return post("/rest/v1/rpc/api_v1_" + name, args, token)


for f in api("findings", p_status="open"):
    print(f["severity"], f["finding_code"] or "", f["title"])

JavaScript

// Node 18 or later: save as findings.mjs and run with node findings.mjs
const BASE = process.env.PENTAL_API_URL;
const ANON = process.env.PENTAL_PUBLISHABLE_KEY;
const KEY = process.env.PENTAL_API_KEY;

async function post(path, body, token) {
  const r = await fetch(BASE + path, {
    method: 'POST',
    headers: { apikey: ANON, Authorization: 'Bearer ' + (token || ANON), 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const text = await r.text();
  if (!r.ok) throw new Error(r.status + ' ' + text);
  return text ? JSON.parse(text) : null;
}

// 1. The key for a password made for this sign-in
const x = await post('/rest/v1/rpc/rpc_api_key_exchange', { p_key: KEY });
// 2. Sign in with it
const session = await post('/auth/v1/token?grant_type=password', { email: x.email, password: x.password });
// 3. Make the session the key's
await post('/rest/v1/rpc/rpc_api_key_bind', { p_key: KEY }, session.access_token);

const api = (name, args = {}) => post('/rest/v1/rpc/api_v1_' + name, args, session.access_token);

for (const f of await api('findings', { p_status: 'open' })) console.log(f.severity, f.finding_code || '', f.title);

Curl

# Needs curl and jq
H=(-H "apikey: $PENTAL_PUBLISHABLE_KEY" -H "Content-Type: application/json")
KEYBODY=$(jq -n --arg k "$PENTAL_API_KEY" '{p_key: $k}')

# 1. The key for a password made for this sign-in
X=$(curl -sS --fail-with-body "${H[@]}" -H "Authorization: Bearer $PENTAL_PUBLISHABLE_KEY" \
  -d "$KEYBODY" "$PENTAL_API_URL/rest/v1/rpc/rpc_api_key_exchange")
# 2. Sign in with it
TOKEN=$(curl -sS --fail-with-body "${H[@]}" -d "$(echo "$X" | jq '{email, password}')" \
  "$PENTAL_API_URL/auth/v1/token?grant_type=password" | jq -r .access_token)
# 3. Make the session the key's
curl -sS --fail-with-body "${H[@]}" -H "Authorization: Bearer $TOKEN" \
  -d "$KEYBODY" "$PENTAL_API_URL/rest/v1/rpc/rpc_api_key_bind" > /dev/null

curl -sS --fail-with-body "${H[@]}" -H "Authorization: Bearer $TOKEN" \
  -d '{"p_status": "open"}' "$PENTAL_API_URL/rest/v1/rpc/api_v1_findings" | jq -r '.[] | "\(.severity) \(.title)"'

Reading

Each read returns a list of rows with named columns. The full list of parameters and columns, with their types, is in the API's OpenAPI document. Engagements, findings and clients come with changed_at, when each was last written, and row_version, and comments with changed_at; p_changed_since returns only rows written since a moment, which is how to keep another system up to date without reading everything each time.

FunctionParametersReturns
api_v1_meNone
api_v1_engagementsp_client_id, p_status, p_changed_since, p_include_archived
api_v1_findingsp_engagement_id, p_client_id, p_status, p_severity, p_changed_since, p_include_drafts, p_include_archived
api_v1_clientsp_search, p_changed_since
api_v1_peoplep_include_inactive
api_v1_commentsp_engagement_id, p_changed_since
api_v1_timelinep_engagement_id, p_changed_since
api_v1_filesp_engagement_id
api_v1_statusesNone

Every parameter of a read can be left out. A list can be cut down the way PostgREST does it, by adding select, order and limit to the address: api_v1_findings?select=id,title,severity&order=severity_rank&limit=50.

Writing

Writing needs a key that reads and writes, and writing switched on under Settings, API. Each write is made as the key's person, under the same rules as the portal: a tester can set a finding's status and a client cannot, a client can ask for a retest of a finding shown to them, and nobody reaches an engagement that is not theirs. The administrator's list counts each key's writes, and a key makes sixty writes a minute at most. Changes made through the API send no email; the portal's own screens do that.

FunctionParametersWhat it does
api_v1_finding_set_statusp_finding_id, p_status, p_row_version
api_v1_finding_request_retestp_finding_id
api_v1_comment_addp_engagement_id, p_content, p_parent_id, p_client_visible
api_v1_client_createp_name, p_industry, p_contact_name, p_contact_email, p_contact_phone, p_address
api_v1_client_updatep_client_id, p_row_version, and any of the fields above
api_v1_engagement_createp_client_id, p_name, p_start_date, p_end_date, p_type, p_lead_id, p_test_days, p_scope
api_v1_engagement_updatep_engagement_id, p_row_version, p_name, p_type, p_start_date, p_end_date, p_lead_id, p_clear_lead, p_test_days, p_scope

When a call is refused

A refusal comes back with an HTTP status and a body whose details holds a short code a program can act on, and whose message says what to do in words.

StatusCodesMeaning
401key_invalid, key_revoked, key_expired, key_suspended
401not_a_key_session, session_ended, no_session, no_exchange
403api_off, role_off, contractors_off, writes_off, read_only
403browser, not_allowed
404not_found
409changed
429rate_limited
400bad_request

Keeping a key safe

Treat a key as you would the person's password. Keep it in your secrets manager or an environment variable, give each system its own key with a name that says what uses it, choose read-only wherever writing is not needed, and the shortest life that suits. Never put a key in a web page or an app that runs on somebody else's device: a request from a browser is refused, because every visitor to the page could read the key. When somebody leaves, switching them off revokes their keys; when a key may have got out, revoke it, and an administrator can revoke every key at once.

The rest of Pental's security is the same for a key as for a person: how Pental keeps your data in your own database, and setting up your team and access for what each role may do.


Connect Pental to the Way Your Team Works

Turn the API on in Settings, make a key on your profile, and have your tracker or project board read your engagements and findings in a few lines of code.

Only need the reporting? Our reporting tool is free and open source, on your own server.


Also Worth Reading

Setup guide

Prefer to watch it?

The whole setup recorded, with chapters you can jump to: registering, your own domain, your own database in any of three places, your own mail server, branding, the first sign-in, updating the database on Supabase Cloud, and moving the database if you change your mind.

  • 0:00 · Registering, signing in, and the free trial
  • 0:44 · Your name and your firm’s name
  • 0:50 · Custom domain
  • +6 more