Test what you own, from the outside.
BetaTillDrill tests your sites and APIs the way an attacker or a traffic spike would, before either arrives. It tests only what you prove you own, and it checks that proof again before every run. A scanner that probed any host would be reconnaissance for attackers, and a load tester that hit any host would be a denial-of-service service, so ownership comes first. Manage targets and read results under Shield → TillDrill.
What you can add
A target is one of two kinds:
- A domain: a bare hostname such as
app.example.com. Its site is tested over https. Pastinghttps://app.example.com/works too, and keeps only the hostname. - A URL: a base URL with its scheme, port and path, such as
https://api.example.com:8443/v1, for an API. It can’t carry a query, a fragment or credentials.
A target has to be reachable from the internet by name. Private and reserved names such as localhost, .local, .internal and .test are refused, and so are bare IP addresses and Till’s own domains. A workspace holds up to 50 targets. Only owners and admins can add, check, run or remove them.
Prove you own it
Each target gets its own value to publish. Any one of these three proves it:
A DNS record
Add a TXT record at _tillshield. plus the hostname, holding the value TillShield shows:
_tillshield.app.example.com. 300 IN TXT "tillshield-verify=<value from TillShield>"A file on the server
Serve the same value as a line of plain text at /.well-known/tillshield-verify on the target’s origin, including its port for a URL target. TillShield reads it with no redirects, from a public address only, and reads at most 4 KB.
# served at https://app.example.com/.well-known/tillshield-verify, status 200, no redirect
tillshield-verify=<value from TillShield>A domain you already verified
A domain this workspace verified for single sign-on covers itself and every subdomain. A TillAuth custom domain covers itself. There’s nothing to publish. Instead, the domain’s own record is checked again, and a TillAuth domain also counts while it still points at TillAuth. A target one of these covers is verified the moment you add it, and that check reads only your own DNS records, never the target.
What each proof allows
A DNS record shows you control the name, but a name can point at anyone’s server. Only the file shows you control the server that answers for it. So the proofs allow different drills:
| Drill | DNS record or verified domain | File on the server |
|---|---|---|
| Passive checks | Yes | Yes |
| Latency checks | Yes | Yes |
| Active checks | Over https only, where the certificate ties the server to the name | Yes |
| Intrusive probes | No | Yes, and only with your authorisation and a window |
| Load drills | No | Yes |
A defense drill exercises a rule rather than a target, so it needs no proof.
Checked again before every run
A proof is only as good as the moment it was seen, so every drill looks for it again before it sends a single request. It tries the method that worked last first. A drill doesn’t start when:
- no proof holds; the target then shows as lapsed if it was proven before;
- the only proof that holds doesn’t allow that drill, such as a DNS record for a load drill;
- a DNS lookup fails on Till’s side, so ownership can’t be checked. The target’s state stays as it was.
Check proof on a target, or tilldev shield drill targets check, looks for the proof straight away. Each check reaches out to the target, so a workspace can ask for 30 an hour, and one target once every 10 seconds. When no proof holds, the target shows what each check saw.
Adding, proving, lapsing and removing a target, changing its schedules or latency settings, recording or withdrawing authorisation for intrusive probes, starting a run by hand, stopping one, and the end of every load drill, with who did it, are all in the workspace audit log. Removing a target retires it: nothing runs against it again, and past runs keep its name. Adding it back gives it a new value to publish.
What any visitor can see
A passive run reads what your site already shows every visitor and judges it against 32 checks. Each check is tagged with the OWASP Top 10 category it falls under, and with ASVS requirements where one fits. A run makes only TLS handshakes, GET requests and DNS lookups, and nothing on the target is changed. Start a run with Run now on a target, from the CLI or the API, or set it to repeat.
A run goes in this order, and stops at the first step that fails:
- It checks the proof again, as above.
- It resolves the hostname. If any address it resolves to is private or reserved, the run is refused and nothing is sent.
- Every connection to the target goes to those checked addresses only, so a DNS change mid-run can’t point it somewhere else. Each connection gives up after 10 seconds, and the whole run after 90 seconds.
- It reads four sections, each on its own, so one that can’t be reached doesn’t hide the others.
HTTP requests carry the user agent TillShield-Drill/1 (+https://tilldev.dev/docs/tillshield/drill), so you can find them in your logs. TillShield keeps the names and attributes of cookies, never their values, and keeps URLs without their query strings.
TLS and certificate
One handshake as a browser would make it, then each protocol version from TLS 1.0 to 1.3 tried on its own. The certificate is checked for trust, for the hostname, for its expiry and for its key size.
| Check | Level | OWASP |
|---|---|---|
Certificate has expireddrill-cert-expired | High | a04, asvs.12.2.2 |
Certificate expires within 14 daysdrill-cert-expiring | Medium | a04, asvs.12.2.2 |
Certificate doesn’t cover this hostnamedrill-cert-name-mismatch | High | a04, asvs.12.2.2 |
Certificate isn’t trusteddrill-cert-untrusted | High | a04, asvs.12.2.2 |
Certificate key is too shortdrill-cert-weak-key | High | a04 |
Served over plain httpdrill-plain-http | High | a04, asvs.12.2.1 |
Doesn’t offer TLS 1.3drill-tls-no-1-3 | Low | a04, asvs.12.1.1 |
Accepts TLS 1.0 or 1.1drill-tls-old-protocol | Medium | a04, asvs.12.1.1 |
Response
A GET of the target, following up to 3 redirects on the same origin. A redirect to another origin is recorded, not followed. Only the status, the security and server headers, and the names and attributes of cookies are kept. The body is never read.
| Check | Level | OWASP |
|---|---|---|
Cookie without a __Host- or __Secure- prefixdrill-cookie-no-prefix | Informational | a02, asvs.3.3.1 |
Cookie has no SameSite attributedrill-cookie-no-samesite | Low | a01, asvs.3.3.2 |
Cookie sent without Securedrill-cookie-not-secure | Medium | a02, asvs.3.3.1 |
Cookie readable by scriptsdrill-cookie-script-readable | Low | a02, asvs.3.3.4 |
No Cross-Origin-Opener-Policy on a pagedrill-coop-missing | Informational | a02, asvs.3.4.8 |
Content-Security-Policy allows evaldrill-csp-eval | Low | a05, asvs.3.4.3 |
Content-Security-Policy allows inline scriptsdrill-csp-inline-scripts | Medium | a05, asvs.3.4.3 |
No Content-Security-Policy on a pagedrill-csp-missing | Medium | a02, asvs.3.4.3 |
Content-Security-Policy leaves plugins or base URLs opendrill-csp-plugins-base | Low | a02, asvs.3.4.3 |
Any site can frame the pagedrill-framing | Medium | a02, asvs.3.4.6 |
No Strict-Transport-Security headerdrill-hsts-missing | Medium | a02, asvs.3.4.1 |
HSTS lasts less than a yeardrill-hsts-short | Low | a02, asvs.3.4.1 |
HSTS doesn’t cover subdomainsdrill-hsts-subdomains | Informational | a02, asvs.3.4.1 |
No X-Content-Type-Options: nosniffdrill-nosniff-missing | Low | a02, asvs.3.4.4 |
No Referrer-Policy headerdrill-referrer-policy | Informational | a02, asvs.3.4.5 |
Response names software versionsdrill-version-disclosed | Low | a02, asvs.13.4.6 |
Plain http
What port 80 answers for the same hostname, and whether it sends visitors to https on that hostname. A URL target with its own port, or one already on http, skips this section.
| Check | Level | OWASP |
|---|---|---|
Plain http doesn’t redirect to httpsdrill-http-no-upgrade | Medium | a04, asvs.12.2.1 |
DNS and mail
MX, SPF, DMARC and CAA records, looking up the name tree for DMARC and CAA, and whether answers are signed with DNSSEC. These are public records any resolver can read.
| Check | Level | OWASP |
|---|---|---|
No CAA recorddrill-caa-missing | Informational | a02 |
Sends or takes mail but has no DMARC policydrill-dmarc-missing | Medium | a02 |
DMARC policy only monitorsdrill-dmarc-none | Low | a02 |
DNS answers aren’t signeddrill-dnssec-off | Informational | a02 |
Takes mail but publishes no SPFdrill-spf-missing | Medium | a02 |
More than one SPF recorddrill-spf-multiple | Medium | a02 |
SPF doesn’t reject other sendersdrill-spf-permissive | Medium | a02 |
Ask the target directly, and read only
Active checks ask your own target questions a passive run can’t: does this path answer, does this folder list its files, what does this API share with another origin, does this link parameter send visitors somewhere else, does this name still point at a provider that claims it. They are judged against 15 checks, tagged the same way as the passive ones.
Everything a run sends is a read. There is no PUT, DELETE, PATCH or CONNECT, no password guessed, no injected payload, nothing written and nothing deleted. Two values a run sends are deliberately unreachable: the origin it uses to ask about cross-origin sharing, and the host it uses to test redirect parameters, both under a name reserved so it can never resolve. A target that honours the redirect sends nobody anywhere real.
A run goes through the same gate as every other drill, and then some of its own:
- The proof is checked again. Active checks need the file on the server, or a DNS record over https, where the certificate ties the server to the name.
- The hostname is resolved once; a private or reserved address refuses the run, and every request is pinned to the addresses it resolved to.
- The target is asked for a path that cannot exist, to learn what it answers for anything missing. That answer is the baseline every later finding is measured against.
- Each part runs on its own, so one that can’t be reached never hides the others, and a part that couldn’t run is reported as not run, never as a pass.
One run sends at most 160 requests, 4 at a time and 60 ms apart, reads at most 4 KB of each answer, and stops after 180 seconds. Requests carry the user agent TillShield-Drill/1 (+https://tilldev.dev/docs/tillshield/drill), so you can find them in your logs, and Stop ends a run within seconds and keeps what it saw. Active checks are off for a new target: turn them on under a target, with tilldev shield drill targets active <target-id> daily, or run one now with tilldev shield drill active <target-id>. When the name resolves nowhere, nothing is sent and only the DNS part runs — which is exactly when a name pointing at nothing matters most.
Paths that answer when they shouldn’t
A short fixed list of paths that leak by accident: version control directories, environment files, database dumps, build manifests and open admin panels. Each is one plain GET. A path counts only when the bytes read match what that file looks like and differ from the page the target serves for anything missing, so a site that answers every path is never reported. Only the hash of those bytes is kept, never the body.
| Check | Level | OWASP |
|---|---|---|
A status or debug endpoint answers without authenticationdrill-exposed-admin | High | a05 |
A backup or database dump is served to the publicdrill-exposed-backup | High | a05 |
An environment or settings file is served to the publicdrill-exposed-env | Critical | a05 |
A dependency manifest is served to the publicdrill-exposed-manifest | Low | a06 |
Version-control metadata is served to the publicdrill-exposed-vcs | High | a05 |
Directory listing
Whether a folder answers with the names of the files inside it, for the site root and a handful of common asset folders.
| Check | Level | OWASP |
|---|---|---|
A directory is served as a list of its filesdrill-directory-listing | Low | a05 |
Methods
One OPTIONS request reads the methods the target states it allows, and one TRACE request shows whether it echoes a request back. Methods that could change something are read from the target’s own answer and reported; they are never sent.
| Check | Level | OWASP |
|---|---|---|
The target advertises methods that change contentdrill-methods-write-advertised | Medium | a05 |
The target echoes requests back with TRACEdrill-trace-enabled | Medium | a05 |
Cross-origin sharing
One GET carrying an Origin header set to a reserved name that cannot exist, to read what the target would share with a page on another origin, and whether it would share credentials with it.
| Check | Level | OWASP |
|---|---|---|
Any site may read responses from this targetdrill-cors-any-origin | Medium | a05 |
Signed-in responses are readable by any sitedrill-cors-credentials-open | Critical | a01 |
The target repeats back whatever origin asksdrill-cors-reflects-origin | Medium | a05 |
Redirect parameters
Common link parameters, each tried once with a reserved host as its value, to see whether the target sends a visitor off-site. The host cannot resolve anywhere, so an honoured redirect reaches nobody, and the redirect is read, never followed.
| Check | Level | OWASP |
|---|---|---|
A parameter sends visitors to any addressdrill-open-redirect | Medium | a01 |
Names pointing at nothing
DNS only, no requests: whether the target’s name, or one of its records, points at a provider that no longer claims it, or at a name that resolves nowhere. Either is a name someone else could claim and serve as yours.
| Check | Level | OWASP |
|---|---|---|
A DNS record points at something that is no longer theredrill-dangling-record | Medium | a05 |
A DNS record points at a service anyone could claimdrill-subdomain-takeover | Critical | a05 |
Advisories for the version it names
When the target names a software version in its own response, public advisories for that version are looked up. These are fingerprints, not proof: the result says so, and a patched build that still names an old version is a false alarm to confirm by hand.
| Check | Level | OWASP |
|---|---|---|
The version the target gives away has published advisoriesdrill-version-advisories | Medium | a06 |
The one part that sends inputs built to be mishandled
Every other drill reads. Intrusive probes send values made to be parsed wrong — a stray quote, a value a template engine would evaluate, an over-long field, a sign-in that must fail — to find where your own target mishandles them. Because they reach the target in a way the other drills never do, they are off until you turn them on, and three gates stand between them and a run.
Opt in, three ways at once
- The strongest proof. Only the file served on the target allows intrusive probes. A DNS record or a domain you verified elsewhere never does, because they prove the name, not the server.
- Your authorisation, in writing. You record that you are authorised to test the target. The authorisation stands for 90 days and then lapses until you record it again; withdrawing it stops the probes and turns off their schedule at once.
- A maintenance window. They run only inside a window you set — a weekday, a start time and a length from 30 minutes to 8 hours, read in the timezone you choose, up to 7 windows a target. A run started outside every window is refused.
With all three in place, run one by hand, or set a schedule to run inside the next window. Only one intrusive run happens at a time across the whole workspace. Every run goes through the same gate as the others first — the proof is checked again, the name resolves once, a private or reserved address refuses the run, and every request is pinned to the addresses it resolved — and then confirms the window is still open.
Where your input lands
Each parameter is sent a harmless one-time marker, and the answer is read to see whether that marker comes back unescaped in the HTML, a header or a redirect. It finds the places untrusted input reaches the page; the marker is inert and carries no script.
| Check | Level | OWASP |
|---|---|---|
A value breaks out of an attribute it is written intodrill-reflected-attribute | Medium | a03 |
A value is written into the page without being encodeddrill-reflected-html | Medium | a03 |
Inputs built to trip a parser
A parameter gets a value with a stray quote, and the answer is read for a database error the target leaks; and a value a template engine would evaluate, read back only to see whether the engine ran it. Nothing is a working exploit: the values are the smallest that reveal mishandling, and a leaked error or an evaluated marker is reported, never used.
| Check | Level | OWASP |
|---|---|---|
A quote reaches the database unescapeddrill-sql-error | High | a03 |
A parameter is evaluated as a server-side templatedrill-template-evaluated | Critical | a03 |
Malformed and oversized values
A parameter is sent an over-long or malformed value to see whether the target answers with a server error or a stack trace instead of handling it. It measures resilience with one request at a time against the baseline, never a flood.
| Check | Level | OWASP |
|---|---|---|
A framework debug page is shown to visitorsdrill-debug-page | High | a05 |
A stack trace is shown to visitorsdrill-stack-trace | Medium | a05 |
Sign-ins that are meant to fail
When the target has a sign-in form, a few deliberately wrong sign-ins are tried against a reserved username at @drill.invalid, which can never be a real account. It reads whether the session cookie is marked secure, HTTP-only and same-site, whether a cross-site request token is present, and whether repeated failures are throttled. No real account is ever touched and no password is guessed.
| Check | Level | OWASP |
|---|---|---|
A sign-in form carries no anti-forgery fielddrill-login-no-csrf | Medium | a01 |
A sign-in form never slows down under repeated failuresdrill-login-no-throttle | Medium | a07 |
A sign-in form submits over plain HTTPdrill-login-over-http | High | a02 |
A session cookie is set without Secure or HttpOnlydrill-session-cookie-insecure | Medium | a05 |
One run sends at most 240 requests, 1 at a time and 200 ms apart, reads at most 64 KB of each answer, and stops after 5 minutes. It reads at most 6 pages to find the 20 parameters it tries, and makes at most 8 failing sign-ins. Requests carry the user agent TillShield-Drill/1 (intrusive; +https://tilldev.dev/docs/tillshield/drill), so you can find them in your logs. A workspace starts 6 intrusive runs a day and one per target every 1 hour. A run stops itself if the target starts failing. Stop ends one within seconds and keeps what it saw.
How fast it answers, from where your users are
A latency run sends 5 ordinary GET requests to the target from each region, 500 ms apart, and times each one: connecting, the TLS handshake, the first byte and the whole answer. It goes through the same steps as a passive run first: the proof is checked again, the hostname is resolved once, a private or reserved address refuses the run, and every request goes to the checked address. A redirect is timed as it is and not followed. Each request gives up after 10 seconds and reads at most 1 MB of the answer, which is never kept.
Every target has a budget: the 95th-percentile total time a request should stay under, 1000 ms unless you set one from 50 ms to 30 seconds. A run lists each region with its 50th and 95th percentile, first byte, connect and TLS times and the answers it got, and judges the result against 6 checks: over budget, a slow first byte (over 800 ms), regions far slower than the rest, failed requests, and the target down from some or every region.
Measure it now with Measure now under Speed and load on a target, or set it to repeat hourly or daily; latency is off for a new target. Pick the regions it runs from, or leave it on every region that can measure when the run starts. A latency run is allowed by any proof, and a whole run ends within 4 minutes. Requests carry the user agent TillShield-Latency/1 (+https://tilldev.dev/docs/tillshield/drill) and the header x-tilldrill-run: <run id>.
How it holds up, with a hard ceiling
A load drill sends a planned rate of requests to one path on the target and records what came back, second by second. It needs the file on the server: a DNS record or a verified domain never allows one, because a name can point at someone else’s server. Plans past a cap are refused, never trimmed, and the caps are the same in the dashboard, the CLI and the API.
| Plan | Allowed |
|---|---|
| Peak rate | 1 to 500 requests a second, split between the regions |
| Requests in flight | Up to 200 |
| Length | 30 to 600 seconds |
| Requests in all | Up to 150,000 |
| Method | GET or HEAD; a load drill never sends a body |
| Path | One path on the target’s own host and port |
| Stop on errors | When 1% to 50% of requests fail; 20% unless set |
| Stop on latency | When the 95th percentile passes 100 ms to 30 seconds; 5 seconds unless set |
| Shape | How the rate moves |
|---|---|
ramp | Climbs from 1 request a second to the peak over the first half, then holds it. The default. |
steps | Climbs to the peak in five equal steps, so you can see the rate where it starts to slow. |
constant | Holds the peak from the first second. |
spike | Sends a tenth of the peak, with the full peak in the middle third. |
It stops on its own
Every region judges itself every 10 seconds, once it has sent at least 20 requests in that window. When the error rate or the 95th percentile passes the plan’s threshold, that region stops and the others are stopped with it. Errors are requests with no answer, 5xx answers and 429s. You can stop a drill at any time with Stop, the CLI or the API, and it keeps what it measured until then. A probe host reports in every 5 seconds while it runs, and stops by itself if it can’t reach TillShield for 20 seconds. A drill that runs past its length plus 3 minutes is stopped.
What a drill shows
The requests sent against the plan, the peak rate reached, the error and rate-limited shares, the 95th percentile at the start and at the peak, and a timeline in 5-second steps of what was answered, what failed and how fast. It is judged against 6 checks, such as responses slowing down sharply under load or the planned rate never being reached.
Requests carry the user agent TillShield-Load/1 (+https://tilldev.dev/docs/tillshield/drill) and the header x-tilldrill-run: <run id>, so you can find them in your logs and leave them out of your analytics. A workspace can start 10 load drills a day, one target once every 15 minutes, and run 1 at a time. Load drills never run on a schedule.
Where the traffic comes from
Latency and load run from TillShield’s probe hosts in 13 regions: Africa (south), Africa (west), Africa (east), Africa (north), Europe (west), Europe (east), Middle East, Asia (south), Asia (east), Oceania, North America (east), North America (west), South America. Each probe host checks every task it is given again before it sends anything: the address must be public, the plan must be inside the caps, and a task it can’t read is refused. A load drill runs only from probe hosts.
When no probe host is online in a region, latency in 9 of them can still be measured from TillShield’s edge, over the same pinned connection to the checked address. The edge can’t reach addresses on its own provider’s network, so for a target there that region comes back not run, with the reason. Each region shows where it really ran from.
Regions in the dashboard, tilldev shield drill regions and GET /shield/drill/regions list which regions can measure now and the addresses their traffic leaves from.
Latency checks
| Check | Level |
|---|---|
The target is down from every regionperf-availability-down | Critical |
The target is unreachable from some regionsperf-availability-region | High |
Some requests failedperf-errors-latency | Low |
Responses are slower than the budgetperf-latency-budget | Medium |
The server takes long to send the first byteperf-latency-first-byte | Low |
Some regions are far slower than othersperf-latency-region-slow | Low |
Load checks
| Check | Level |
|---|---|
The load drill stopped because requests failedperf-capacity-abort-errors | High |
The load drill stopped because responses got too slowperf-capacity-abort-latency | Medium |
Responses slow down sharply under loadperf-capacity-degraded | Low |
The planned rate was never reachedperf-capacity-missed-target | Medium |
More than 1% of requests failed under loadperf-errors-load | Medium |
A rate limit answered part of the loadperf-errors-rate-limited | Informational |
Reading a run
Each check comes out one of four ways:
| Result | Means |
|---|---|
| Fail | What it looks for is there. The result shows what was seen, such as the header value or the cookies at fault, and the check says how to fix it. |
| Pass | What it looks for isn’t there. |
| Doesn’t apply | The check has nothing to judge on this target, such as cookie checks on a response with no cookies, or TLS checks on an http target. |
| Not run | Its section or part couldn’t be collected on this run, and the reason is shown. Not run is never counted as a pass. |
A run that couldn’t collect any section fails, and still keeps each check’s reason. A run that was refused, because the proof had gone, the target was removed or an address was private, sends no checks and says why. A run that stops without finishing is marked failed after about 4 minutes, and you can start it again.
Run it on its own, hear when it gets worse
Each target repeats its passive checks daily, weekly or not at all, and a new target is set to daily. A proven target that has never run starts straight away, and after that runs one period after its last run. A lapsed target stays on its schedule: each scheduled run checks the proof again and is refused until the proof is back. Active checks have their own schedule, daily, weekly or off, and a new target has them off, so a target proven for passive checks alone never starts asking the server on its own. Latency has its own schedule too, hourly, daily or off, and both work the same way. Intrusive probes can repeat daily or weekly as well, but only once you’ve turned them on, and each run waits for the next maintenance window.
Scheduled runs alert on what changed. When a scheduled passive, active, intrusive or latency run finds checks failing at medium or above that didn’t fail on the last successful run of the same kind, it sends one alert listing them, and run.profile says which kind it was. The first run of a target counts every failure as new. A run you started yourself never alerts about failing checks, because you’re already looking at it. Any run, scheduled or started by hand, that finds the proof of a verified target gone sends one alert as the target lapses. A proof check you start yourself shows its result instead.
Alerts go where your other security alerts go: the email, Slack and webhook set under Shield → Settings → Playbooks & security alerts. See security alerts for who gets email and how to verify a webhook’s signature.
| Webhook type | Sent when |
|---|---|
tillshield.drill.checks_failing | A scheduled run finds checks newly failing at medium or above. |
tillshield.drill.target_lapsed | A run finds that a verified target’s proof no longer holds. reason says what each check saw. |
{
"type": "tillshield.drill.checks_failing",
"delivery_id": "6f1c…",
"target": { "id": "9b2e…", "value": "app.example.com" },
"run": { "id": "d41a…", "trigger": "schedule", "profile": "surface" },
"failures": [
{ "id": "drill-cert-expiring", "title": "Certificate expires within 14 days", "level": "medium" }
],
"reason": null,
"checked_at": "2026-10-01T06:00:12.000Z",
"link": "https://tilldev.dev/acme/shield/drill?target=9b2e…"
}How much you can run
| What | Limit |
|---|---|
| Runs started by hand, per workspace | 20 an hour |
| Runs of one target started by hand | One of each kind every 5 minutes |
| Runs waiting or running, per workspace | 5 at once |
| Runs waiting or running, per target | One of each kind at a time |
| One passive run | 90 seconds |
| Active runs, per workspace | 50 a day, 2 at a time |
| Active runs of one target | One every 15 minutes |
| One active run | 180 seconds and 160 requests |
| Intrusive runs, per workspace | 6 a day, 1 at a time, inside a window only |
| Intrusive runs of one target | One every 1 hour |
| One intrusive run | 5 minutes and 240 requests |
| Maintenance windows, per target | 7, each 30 minutes to 8 hours |
| One latency run | 4 minutes |
| Load drills, per workspace | 10 a day, 1 at a time |
| Load drills of one target | One every 15 minutes |
| Defense drills, per workspace | 50 a day, 3 at a time |
| Defense drills of one rule | One every 2 minutes, one at a time |
Scheduled runs don’t count toward the hourly limit.
Read your own APK, IPA and desktop builds
TillDrill also reads the builds you ship and judges them against 41 checks, each tagged with the OWASP MASVS control it falls under. It flags debuggable and test builds, plain-text traffic, components any app on the device can open, secret keys shipped inside the app, executables built without the usual hardening, and weak crypto, each with how to fix it. A build is your own file, so no proof of ownership is needed. Scan one under Shield → TillDrill → App builds, from the CLI, or from the API.
| Upload | Read as |
|---|---|
| An APK | Android: manifest, network config, components, signing, DEX strings and native libraries. |
| An IPA | iOS: Info.plist, App Transport Security, entitlements, provisioning profile and every executable. |
| A zipped .app | macOS: the same as an IPA, plus the hardened runtime and library validation. |
| A zip of a Windows or Linux build | Every .exe, .dll, .so and Linux executable inside, and the text files beside them. |
| One executable | An ELF, PE or Mach-O file on its own. |
An Android App Bundle, a DMG, an MSI and Linux packages aren’t read as they are; the scan fails and says how to get a file it reads, such as a universal APK from bundletool build-apks --mode=universal. A build can be up to 1024 MB, sent in parts of 8 MB. A release asset TillForge stored can be scanned without uploading it again.
What is kept
Each build is read once and deleted, and only the report is kept: the app’s id and version, its settings, how each executable was built, and how each check came out. A key found in the code is kept masked, with its first few characters and its length, never in full. A scan reads at most 768 MB of uncompressed content and 4000 files, and a section it stopped short in says so. A scan that runs past 10 minutes fails. Removing a scan removes its report; the audit log keeps who started it and when.
Android manifest and signing
The compiled manifest, the network security config it points to, the components each app can reach, and the signature schemes the APK carries.
| Check | Level | MASVS |
|---|---|---|
App data is included in backups without rulesapp-android-backup | Low | storage.2 |
Plain http allowed for some domainsapp-android-cleartext-domains | Low | network.1 |
Android app allows plain httpapp-android-cleartext | Medium | network.1 |
Android build is debuggableapp-android-debuggable | High | resilience.4 |
Installs on Android versions before 7.0app-android-old-min | Low | code.1 |
Targets an Android version older than 14app-android-old-target | Medium | code.1 |
Activity reachable by every appapp-android-open-activity | Low | platform.1 |
Content provider open to every appapp-android-open-provider | High | platform.1 |
Service or receiver open to every appapp-android-open-service | Medium | platform.1 |
Android build is marked test-onlyapp-android-test-only | Medium | resilience.4 |
APK is not signedapp-android-unsigned | High | resilience.2 |
Release build trusts user-installed certificatesapp-android-user-certs | Medium | network.1 |
Signed with the v1 scheme onlyapp-android-v1-signing | Medium | resilience.2 |
Apple settings and entitlements
Info.plist, App Transport Security, the entitlements signed into the main executable, and the embedded provisioning profile.
| Check | Level | MASVS |
|---|---|---|
App Transport Security is turned offapp-apple-ats-arbitrary | Medium | network.1 |
Plain http allowed for some domainsapp-apple-ats-insecure-domains | Low | network.1 |
TLS 1.0 or 1.1 allowed for some domainsapp-apple-ats-old-tls | Low | network.1 |
Web content may load over plain httpapp-apple-ats-web | Low | network.1 |
Documents folder shared through Finderapp-apple-file-sharing | Low | storage.2 |
Build lets debuggers attachapp-apple-get-task-allow | High | resilience.4 |
Apple build is not code signedapp-apple-unsigned | High | resilience.2 |
Custom URL schemes registeredapp-apple-url-schemes | Informational | platform.1 |
Mac app honours DYLD environment variablesapp-macos-dyld-env | Medium | resilience.2 |
Mac app loads libraries signed by anyoneapp-macos-library-validation-off | Medium | resilience.2 |
Mac app built without the hardened runtimeapp-macos-no-hardened-runtime | Medium | resilience.2 |
Mac app may run unsigned code from memoryapp-macos-unsigned-memory | Low | code.4 |
Executables
How each executable and library was built: PIE, NX, RELRO, stack canaries, ASLR, CFG, run paths and signatures, for ELF, PE and Mach-O, up to 64 per build.
| Check | Level | MASVS |
|---|---|---|
Executable stack or dataapp-native-exec-stack | High | code.4 |
64-bit Windows binary with low-entropy ASLRapp-native-low-entropy | Low | code.4 |
Windows binary without ASLRapp-native-no-aslr | Medium | code.4 |
Native code without stack canariesapp-native-no-canary | Low | code.4 |
Windows binary without Control Flow Guardapp-native-no-cfg | Informational | code.4 |
Executable is not position independentapp-native-no-pie | Medium | code.4 |
Library or executable without RELROapp-native-no-relro | Low | code.4 |
Libraries searched for outside the appapp-native-rpath | Low | code.4 |
Executable without a code signatureapp-native-unsigned | Medium | resilience.2 |
Keys, secrets and crypto
Strings in the code (DEX and native) and in bundled text files, searched for secret keys, keys meant for clients, weak ciphers and hashes, and unsafe APIs.
| Check | Level | MASVS |
|---|---|---|
Client key embedded in the appapp-client-key | Low | crypto.2 |
Hostname verification turned offapp-hostname-verifier | High | network.1 |
SHA1PRNG random generator in the codeapp-insecure-random | Low | crypto.1 |
Keychain items readable while the device is lockedapp-keychain-always | Medium | storage.1 |
Secret embedded in the appapp-secret-embedded | High | crypto.2 |
Weak cipher or mode in the codeapp-weak-cipher | Medium | crypto.1 |
MD5 or SHA-1 in the codeapp-weak-hash | Informational | crypto.1 |
New since the last build
A scan compares itself with the last successful scan of the same app on the same platform, matched by package name or bundle id, and marks the failing checks that passed there as new. Results read the same way as a passive run: fail, pass, doesn’t apply, or not run when a section couldn’t be read.
In CI
apps upload --wait exits 1 when the build couldn’t be read or a check at the --fail-on level or above fails. With --new-only, only failures the app’s last build didn’t have count, so a release fails on what it made worse.
# after the release build, before it ships
tilldev shield drill apps upload app/build/outputs/apk/release/app-release.apk --wait --new-only --fail-on highA workspace can start 30 scans an hour and have 3 uploading, waiting or being read at once.
Prove your rules still fire
A defense drill tests your detection, not your servers. It takes one TillTell rule, replays the rule’s own smallest test that fires into this workspace’s security stream, and times how long TillTell takes to record the finding and how long the response takes. Nothing leaves Till and no target is needed. Start one under Shield → TillDrill → Defense drills, from the CLI, or from the API.
- The drill sends at most 200 events, 20 ms apart. Each is labelled as a drill and signed by Till. An event that claims to be a drill without that signature is handled as real traffic, so the label can’t be used to hide an attack.
- Drill events are correlated apart from real traffic and from every other drill. They can’t set off a real detection or mask one. The findings they make are drill findings in drill incidents: they never alert and stay out of the backlog.
- Every playbook step a drill finding starts is simulated. Nothing is blocked, revoked or isolated.
- Only rules over the event stream can be drilled. A draft rule records nothing and a paused one isn’t running, so neither can be drilled. A rule that groups by project writes its events to a project you pick, or the workspace’s oldest.
| Outcome | Means |
|---|---|
| Detected | TillTell recorded a finding of the rule within 5 minutes. Time to detect counts from the moment the events were written. |
| Playbook ran | The playbook the rule suggests ran to the end, steps simulated, within 10 minutes. Time to respond counts to its last step. |
| Incident only | The finding reached an incident and no playbook ran, with why. |
| No response | The finding was recorded and nothing responded, with why: the rule isn’t live, or suggests no playbook. |
| Response timed out | A playbook was proposed and hadn’t finished 10 minutes later, often because a step waits for approval. |
| Missed | Nothing was recorded in time. The rule didn’t fire, or the stream is behind. The drill fails with the reason. |
Proof on the coverage map
A detected drill marks the rule’s ATT&CK techniques as proven on TillTell’s coverage map for 90 days. The proof belongs to the rule version it drilled: change the rule and the proof goes until it is drilled again. A rule moved back to draft proves nothing.
In CI, defense <rule-id> --wait exits 1 when the drill is missed or refused, so a change that breaks detection fails before it ships. Waiting stops after 20 minutes, and the drill carries on.
The ports your cloud addresses answer on
Every other drill tests a site. An exposure scan tests a machine: one public address, and the ports it answers on. The address is never typed in. Addresses come from the cloud accounts you connect under Posture, and the account that lists an address is the proof that it is yours. Read them under Shield → TillDrill → Exposure.
Where an address comes from
- A sweep reads each connected account’s own inventory for the public addresses it holds. Reading an inventory touches nobody’s network.
- An address arrives with scanning off. You turn it on for the addresses you want watched, daily or weekly. An address the account stops holding is retired.
- A scan reads the account again before it makes a single connection, and is refused unless the account confirmed the address in the last 7 days. No other proof allows an exposure scan, and this proof allows nothing else.
- A workspace holds up to 250 addresses.
What a scan does
It opens a TCP connection to each of 105 well-known ports, 8 at a time and 40 ms apart, reads up to 512 bytes of whatever the service announces on connection, completes the handshake where the port speaks TLS so the certificate can be read, and closes. One scan makes at most 140 connections and stops after 4 minutes.
Which ports answer
Each port on the list gets one connection. A port counts as open only when the handshake completed; everything else is recorded as refused or timed out. A port the catalogue says belongs on an internal network is called out whatever is behind it.
| Check | Level | OWASP |
|---|---|---|
A cache or key-value store answers the public internetdrill-exposure-cache-open | Critical | a05 |
A container or cluster control port answers the public internetdrill-exposure-container-control | Critical | a05 |
A database port answers the public internetdrill-exposure-database-open | Critical | a05 |
A debug or tooling port answers the public internetdrill-exposure-debug-port | High | a05 |
A service meant for an internal network answers the public internetdrill-exposure-internal-service | Medium | a05 |
A proxy port answers the public internetdrill-exposure-open-proxy | High | a05 |
Remote desktop answers the public internetdrill-exposure-remote-desktop | High | a07 |
A cleartext remote shell answers the public internetdrill-exposure-remote-shell | Critical | a02 |
SSH answers the public internetdrill-exposure-ssh-open | Low | a07 |
This address answers on many portsdrill-exposure-wide-surface | Medium | a05 |
Windows remote management answers the public internetdrill-exposure-windows-management | High | a05 |
What is answering
Whatever the service announces when the connection opens is read and matched against known banners to name the software and, where it says so, the version. Nothing is asked for and no command is sent. A protocol with no encryption of its own is reported as carrying whatever it carries in the clear.
| Check | Level | OWASP |
|---|---|---|
A service that carries credentials in the clear is reachabledrill-exposure-cleartext-service | High | a02 |
A service announces its exact versiondrill-exposure-version-disclosed | Informational | a05 |
The certificate a TLS port serves
Where a port speaks TLS the handshake completes and the certificate is read: the names on it, who issued it, when it expires, how strong its key is, the version negotiated, and whether it covers the address that was scanned.
| Check | Level | OWASP |
|---|---|---|
A port serves an expired certificatedrill-exposure-tls-expired | High | a02 |
A port serves a certificate nothing vouches fordrill-exposure-tls-self-signed | Medium | a02 |
A port negotiates a retired TLS versiondrill-exposure-tls-weak-protocol | High | a02 |
Your own rules, next to what answered
The rules in the connected account that open a port to the whole internet are read from the account, not from the network, and lined up against what actually answered: a port that answers with no rule that opens it, and a rule that opens a port with nothing behind it. Where a provider cannot tie its rules to one address, the scan says so instead of guessing.
| Check | Level | OWASP |
|---|---|---|
A port answers that no rule in the account opensdrill-exposure-undeclared-port | High | a05 |
A firewall rule opens a port that nothing answersdrill-exposure-unused-rule | Low | a05 |
What changed since the last scan
The ports that answered are compared with the last scan of the same address, so a port that has opened since then is a finding of its own, and one that has closed is recorded too.
| Check | Level | OWASP |
|---|---|---|
A port is answering that was closed on the last rundrill-exposure-newly-open | Medium | a05 |
Scanning is off for a new address. A scheduled scan that finds a new failure at medium or above sends a security alert; a scan you start by hand never does. A workspace can have 3 scans waiting or running, start 60 a day, and scan one address once every 15 minutes. Stop ends a scan within seconds and keeps what it saw.
From the terminal
tilldev shield drill targets add app.example.com
tilldev shield drill targets add https://api.example.com/v1
tilldev shield drill targets show <target-id> # the record and the file that prove it
tilldev shield drill targets check <target-id> # exits 1 while no proof holds
tilldev shield drill targets # verified, lapsed or unverified
tilldev shield drill targets rm <target-id>
tilldev shield drill targets schedule <target-id> daily # or weekly, or off
tilldev shield drill run <target-id> --wait # exits 1 on a failing check at medium or above
tilldev shield drill run <target-id> --wait --fail-on high
tilldev shield drill runs --target <target-id>
tilldev shield drill runs show <run-id> --all # every check, not only what needs attention
tilldev shield drill checks
tilldev shield drill active <target-id> --wait # asks the target directly, reading only
tilldev shield drill targets active <target-id> weekly # or daily, or off
tilldev shield drill stop <run-id> # an active run stops within seconds
tilldev shield drill targets intrusive <target-id> on # record your authorisation; it expires after 90 days
tilldev shield drill targets intrusive <target-id> window add --day Sun --start 02:00 --minutes 120 --tz Africa/Kigali
tilldev shield drill targets intrusive <target-id> schedule weekly # runs inside a window; or daily, or off
tilldev shield drill intrusive <target-id> --wait # runs now, only inside an open window with authorisation on
tilldev shield drill regions # where latency and load run from, and their addresses
tilldev shield drill latency <target-id> --wait # exits 1 on a failing check at medium or above
tilldev shield drill latency <target-id> --regions africa-west,europe-west --budget 600
tilldev shield drill targets latency <target-id> --schedule hourly --budget 800 --regions all
tilldev shield drill load <target-id> --peak 100 --duration 120 --path /health --wait
tilldev shield drill load <target-id> --peak 300 --duration 300 --shape steps --abort-errors 5 --abort-p95 2000 --yes
tilldev shield drill stop <run-id> # a load drill keeps what it measured
tilldev shield drill runs --profile load
tilldev shield drill apps upload app-release.apk --wait # exits 1 on a failing check at medium or above
tilldev shield drill apps upload App.ipa --wait --new-only # only checks the app's last build passed
tilldev shield drill apps releases # release assets TillForge stored
tilldev shield drill apps from-release <asset-id> --wait
tilldev shield drill apps # newest first; --app <app-id> for one app
tilldev shield drill apps show <scan-id> --all
tilldev shield drill apps checks
tilldev shield drill defense # rules that can be drilled, and when each was last proven
tilldev shield drill defense tell-cred-stuffing --wait # exits 1 when TillTell records nothing
tilldev shield drill defense show <run-id> # time to detect and to respond
tilldev shield drill exposure sweep # read the connected cloud accounts for addresses
tilldev shield drill exposure # each address, and the ports that answered
tilldev shield drill exposure scan <address-id> --wait # exits 1 on a failing check at medium or above
tilldev shield drill exposure schedule <address-id> weekly # or daily, or offA bare hostname is added as a domain, and a value starting with http:// or https:// as a URL. run --wait follows the run, prints what needs attention by section, and exits 1 when the run is refused or fails, or when a check at the --fail-on level or above fails. The level is medium unless you set it, and none never fails. active --wait, latency --wait and load --wait do the same; load asks before it starts unless you pass --yes, and Ctrl-C while it waits stops the drill. Waiting stops after 15 minutes, and the run carries on. The same operations are in the API reference under TillDrill.