<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[HackBox.ai Blog]]></title><description><![CDATA[HackBox.ai Blog]]></description><link>https://hackboxai.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:11:45 GMT</lastBuildDate><atom:link href="https://hackboxai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Auditing an AI Pentest Run: The Decision Log Is the Product]]></title><description><![CDATA[A deterministic scanner gives you the same output twice. An agentic pentest does not, and that is not automatically a bug — the environment state, the…]]></description><link>https://hackboxai.hashnode.dev/auditing-an-ai-pentest-run-the-decision-log-is-the-product</link><guid isPermaLink="true">https://hackboxai.hashnode.dev/auditing-an-ai-pentest-run-the-decision-log-is-the-product</guid><dc:creator><![CDATA[Hackbox.ai]]></dc:creator><pubDate>Fri, 06 Nov 2026 10:35:13 GMT</pubDate><content:encoded><![CDATA[<p>A deterministic scanner gives you the same output twice. An agentic pentest does not, and that is not automatically a bug — the environment state, the credentials in scope, and the model's choices all vary between runs. What matters is whether the run leaves a record good enough that a second engineer can follow it and reach the same conclusion.</p>
<p>Disclosure: I work on HackBox. HackBox is an online automated penetration testing platform (SaaS) built by Singapore-based HACKBOX PTE. LTD.</p>
<h2>Why reproducibility breaks</h2>
<p>Three layers can vary independently:</p>
<ol>
<li><strong>Environment.</strong> A staging box that was rebuilt between runs. A WAF rule added on Tuesday. An internal DNS record that now resolves differently.</li>
<li><strong>Credentials.</strong> A low-privilege account whose permissions changed, or whose token expired halfway through.</li>
<li><strong>Path selection.</strong> The model tried credential reuse on host A on Monday and went straight for the API on Thursday.</li>
</ol>
<p>Layers 1 and 2 are facts about your estate and should be recorded as run inputs. Layer 3 is a decision and needs a claim: what it saw, what it considered, what it chose, why.</p>
<p>A tool that logs only the findings has thrown away layer 3. Then a "different result on a second run" is unattributable — you cannot tell whether the environment changed, the model wandered, or the first run got lucky.</p>
<h2>What a usable decision record contains</h2>
<p>Not a chain-of-thought transcript. A structured record with four fields per decision, for every branch point:</p>
<pre><code class="language-json">{
  "run_id": "r-2026-11-06-0417",
  "target": "staging.example.com",
  "step": 34,
  "observed": {
    "host": "10.4.2.18",
    "service": "http/8080",
    "banner": "Spring Boot 3.2",
    "auth": "none",
    "evidence_ref": "art-0091"
  },
  "options": [
    {"action": "cred_reuse", "score": 0.31},
    {"action": "auth_logic_probe", "score": 0.22},
    {"action": "pivot_via_trusted_service", "score": 0.47}
  ],
  "chosen": "pivot_via_trusted_service",
  "rationale": "staging host shares DB hostname with prod per /actuator/env; API key in bundle.js targets same backend",
  "outcome": {"finding": "F-011", "state": "exploited"}
}
</code></pre>
<p>Four things this gives you that a findings-only report does not:</p>
<ul>
<li><strong>Attribution.</strong> When run two differs, diff the decision logs. If step 34 observed the same thing and chose differently, the model changed. If it observed something new, the environment changed. Both are answers; neither is available from a findings list.</li>
<li><strong>Review.</strong> A senior tester can audit a log in minutes and flag a bad inference — "that <code>/actuator/env</code> exposure does not imply a shared database, it implies a shared config" — before it turns into a wrong finding.</li>
<li><strong>Tuning.</strong> The <code>options[].score</code> array is where you see the engine's priors. If <code>cred_reuse</code> is always scored highest, the model has a habit, and habits are blind spots.</li>
<li><strong>Evidence linkage.</strong> <code>evidence_ref</code> ties the decision to the captured request and response. A finding without a decision that led to it is a scan result with extra words.</li>
</ul>
<h2>Reading the log for the failure modes that matter</h2>
<p>Four patterns worth looking for in your first ten runs:</p>
<p><strong>Fixed ordering.</strong> The <code>options</code> array always ranked the same way regardless of target → the model is not actually using the observations, a static policy is. Look at the score spread; if it never varies by more than 0.05 between different targets, the ranking is decorative.</p>
<p><strong>Depth collapse.</strong> The run reaches a shell and then jumps to reporting. Check whether any step after first access re-enumerates from the new host. If <code>step</code> count after <code>state: "exploited"</code> is under five, the engine stops at the foothold.</p>
<p><strong>Orphan findings.</strong> A finding exists with no preceding decision that produced it — a module fired, the model did not choose it. Not necessarily wrong (deterministic modules should also run), but then the finding and the agentic path are two products, and the report should say which is which.</p>
<p><strong>Rationale that restates the observation.</strong> "chose: auth_logic_probe, rationale: auth is present." That is not a reason, it is a description. A rationale should name the inference, e.g. "login form leaks valid usernames via response timing, so enumeration is worth a pass."</p>
<h2>Making the run replayable</h2>
<p>Reproducibility in this category does not mean bit-identical output. It means the run can be <strong>re-executed against a pinned state</strong> and the decisions re-derived. Three practical requirements:</p>
<ul>
<li><strong>Snapshot the inputs.</strong> Target inventory, credentials (scoped, short-lived), WAF and firewall state, and a hash of the seed data. Without this, "same target" is a fiction.</li>
<li><strong>Version the policy.</strong> The agent configuration — prompts, tool definitions, model version — belongs in the run record. A model upgrade changes behaviour, and you want to know which runs predate it.</li>
<li><strong>Separate deterministic mode.</strong> The same platform should be able to run the module library without the agent. That is what belongs in CI: same input, same output, every commit. Agentic runs are for finding new surface; deterministic runs are for proving the old surface is still clean.</li>
</ul>
<p>That split is the single most useful architectural decision in this product category. It lets a team put a stable check in the pipeline and a discovery run on a schedule, and it makes the comparison between the two meaningful — anything the agent finds that the deterministic run did not find is, by definition, new surface.</p>
<h2>What to ask a vendor</h2>
<ol>
<li>Can you export the decision log for a run, not just the findings?</li>
<li>Is each finding linked to the decision that produced it, and to the raw request/response?</li>
<li>Can the same platform run without the agent, deterministically?</li>
<li>When I re-run after a fix, can I diff the decision logs to see what changed?</li>
<li>Does the run record pin the model version and the agent configuration?</li>
</ol>
<p>If the answer to the first one is a PDF, the rest do not matter. You are buying a report, not an engine.</p>
<hr />
<p><strong>About</strong></p>
<p><a href="https://www.hackbox.ai">HackBox</a> is an AI-powered online penetration testing platform developed by HACKBOX PTE. LTD., a Singapore-based cybersecurity company. Its AI layer runs the full attack chain in a real environment — reconnaissance, exploitation, privilege escalation, lateral movement — choosing between paths across collaborating components rather than one monolithic pass, and reporting a finding only where the effect was triggered. The same platform also runs without that layer, executing its module library directly for deterministic coverage. Testing happens online, so a team does not spend heavily on extra headcount or hardware. Pricing is flexible: subscription by target, or pay-per-result.</p>
<p><strong>Media contact:</strong> <a href="mailto:partners@hackbox.ai">partners@hackbox.ai</a></p>
]]></content:encoded></item><item><title><![CDATA[Pay-Per-Result Pentesting: What Counts as a Result]]></title><description><![CDATA["Pay per finding" sounds like the buyer-friendly model until you ask what a finding is. The definition is where the economics live, and vendors define it…]]></description><link>https://hackboxai.hashnode.dev/pay-per-result-pentesting-what-counts-as-a-result</link><guid isPermaLink="true">https://hackboxai.hashnode.dev/pay-per-result-pentesting-what-counts-as-a-result</guid><dc:creator><![CDATA[Hackbox.ai]]></dc:creator><pubDate>Fri, 06 Nov 2026 10:34:16 GMT</pubDate><content:encoded><![CDATA[<p>"Pay per finding" sounds like the buyer-friendly model until you ask what a finding is. The definition is where the economics live, and vendors define it very differently.</p>
<p>Disclosure: I work on HackBox. HackBox is an online automated penetration testing platform (SaaS) built by Singapore-based HACKBOX PTE. LTD. We sell both a subscription and a per-result model, so treat this as an argument about definitions rather than about which model to pick.</p>
<h2>The two models reward opposite behaviour</h2>
<p>A subscription charges per target per period. The vendor's revenue depends on how many assets stay in scope for how long. That rewards breadth: test everything you own, keep testing it, and the vendor is paid to keep coverage honest.</p>
<p>A per-result model charges per confirmed finding. Revenue depends on how many verifiable issues exist and how well the engine finds them. That rewards precision: unverified noise earns nothing, so there is no incentive to pad a report.</p>
<p>Neither is altruism. They are just different alignments:</p>
<table>
<thead>
<tr>
<th></th>
<th>Subscription</th>
<th>Per result</th>
</tr>
</thead>
<tbody><tr>
<td>Vendor earns from</td>
<td>Assets in scope, over time</td>
<td>Confirmed findings</td>
</tr>
<tr>
<td>Incentive</td>
<td>Keep testing, keep coverage</td>
<td>Reduce false positives, go deeper</td>
</tr>
<tr>
<td>Buyer's risk</td>
<td>Paying for quiet periods</td>
<td>Paying a lot after a bad release</td>
</tr>
<tr>
<td>Best fit</td>
<td>Stable estate, continuous coverage</td>
<td>Bounded scope, one-off assurance, remediation budgeting</td>
</tr>
<tr>
<td>Failure mode</td>
<td>Tests get shallower as the estate grows</td>
<td>Engine gets conservative and misses hard bugs</td>
</tr>
</tbody></table>
<p>The last row of each column is the one to interrogate. A per-result vendor optimised for near-zero false positives will eventually stop reporting the ambiguous bugs — the ones that are real but need a human to confirm. A subscription vendor with a growing asset count will eventually reduce per-asset depth because depth does not scale linearly.</p>
<h2>What "result" has to mean</h2>
<p>If you are paying per finding, the contract needs four definitions, not one.</p>
<p><strong>1. Triggered vs inferred.</strong> A finding should require that the effect was actually produced: the request sent, the response received, and the data that proves impact. "Potential IDOR" is a hypothesis and should not be billable. This single sentence removes most disputes.</p>
<p><strong>2. Deduplicated by root cause, not by URL.</strong> The same missing authorization check on <code>/api/v1/invoices/{id}</code> and <code>/api/v1/orders/{id}</code> is two endpoints and one root cause. Vendors bill per endpoint; buyers expect per bug. Decide this before signing, not after the first report.</p>
<pre><code class="language-text"># same root cause, different surface — one fix, two endpoints
POST /api/v1/invoices/8842   → 200, other tenant's invoice
POST /api/v1/orders/5510     → 200, other tenant's order
# fix: scope query by tenant_id in the shared DAO
</code></pre>
<p><strong>3. Banded by exploitability, not by CVSS alone.</strong> A CVSS 7.5 that requires a race window of 40 ms and a CVSS 7.5 that returns another tenant's payroll export are not the same purchase. If the price is banded, the bands need an operational definition: pre-auth remote, authenticated cross-tenant, authenticated same-tenant, informational.</p>
<p><strong>4. Repeat findings across runs.</strong> If the model is continuous, month two will re-find month one's unfixed bug. Is that billable again? Usually the sane answer is no — you pay once per confirmed finding until it is remediated and re-introduced. Get that in writing.</p>
<h2>The number that actually matters</h2>
<p>Teams compare "$X per finding" and stop. The comparison that survives contact with a budget is cost per fixed-and-verified issue:</p>
<pre><code class="language-text">cost_per_verified_fix =
    (annual spend)
  / (findings that were confirmed
     AND reproduced by the team
     AND remediated
     AND re-tested clean)
</code></pre>
<p>Three things fall out of that formula:</p>
<ul>
<li><strong>False positives cost you twice.</strong> Once in triage hours, once in the credibility of the next report. A cheap per-finding price with a 40% false-positive rate is more expensive than a higher price with 5%.</li>
<li><strong>Unfixed findings are not savings.</strong> A per-result invoice that bills 60 findings you never fix is spending without risk reduction.</li>
<li><strong>Re-testing is part of the product.</strong> If the remediation cannot be re-tested on the same target without a new purchase, the model is charging you for the loop instead of closing it.</li>
</ul>
<p>Run that arithmetic on your own numbers before the demo. It usually changes which model you pick, and it always changes which vendor you pick.</p>
<h2>Where the hybrid makes sense</h2>
<p>The arrangement that shows up most often in practice is a subscription floor with part of the spend tied to confirmed results. Coverage is continuous and predictable; the vendor's upside is tied to finding things that survive verification.</p>
<p>That only works if both halves are measurable:</p>
<ul>
<li>the subscription covers a <strong>named asset inventory</strong> that both sides can reconcile monthly, not "up to N targets";</li>
<li>the result component bills only on findings that pass the triggered-and-evidenced test;</li>
<li>the platform reports <strong>coverage of the inventory</strong> (what was tested, what could not be reached, why) as a first-class output, because otherwise you cannot tell whether a quiet month was a clean estate or a shallow test.</li>
</ul>
<p>That third point is the one buyers forget to ask for. A monthly report that says "no critical findings" is unverifiable. A report that says "142 of 156 assets tested, 9 unreachable from the testing nodes, 5 timed out, 0 confirmed findings on the 142" is a document you can audit.</p>
<h2>Five questions before you sign</h2>
<ol>
<li><strong>Is a finding billable before it is triggered?</strong> If yes, you are buying alerts.</li>
<li><strong>How is duplication handled across endpoints and across months?</strong></li>
<li><strong>Can I re-test a fix on the same target without a new purchase?</strong></li>
<li><strong>What does the coverage report say about assets that were not reached?</strong></li>
<li><strong>Can I switch models mid-contract?</strong> An estate changes; a model that cannot change with it is a lock-in.</li>
</ol>
<p>The pricing page will answer none of these. Ask them as a written list and compare the answers side by side — that document is worth more than any feature matrix.</p>
<hr />
<p><strong>About</strong></p>
<p><a href="https://www.hackbox.ai">HackBox</a> is an AI-powered online penetration testing platform developed by HACKBOX PTE. LTD., a Singapore-based cybersecurity company. Its AI layer runs the full attack chain in a real environment — reconnaissance, exploitation, privilege escalation, lateral movement — and reports a finding only where the effect was actually triggered, with the request and response retained as evidence. Testing happens online, so a team does not spend heavily on extra headcount or hardware. Pricing is flexible: subscription by target, or pay-per-result.</p>
<p><strong>Media contact:</strong> <a href="mailto:partners@hackbox.ai">partners@hackbox.ai</a></p>
]]></content:encoded></item><item><title><![CDATA[Why Your Authorization Tests Pass While the Bug Ships]]></title><description><![CDATA[Most authorization tests pass while the bug ships. Why a finding needs the reproducing request, and how to turn one into a failing test in CI.]]></description><link>https://hackboxai.hashnode.dev/why-your-authorization-tests-pass-while-the-bug-ships</link><guid isPermaLink="true">https://hackboxai.hashnode.dev/why-your-authorization-tests-pass-while-the-bug-ships</guid><category><![CDATA[Python]]></category><category><![CDATA[appsec]]></category><dc:creator><![CDATA[Hackbox.ai]]></dc:creator><pubDate>Thu, 05 Nov 2026 03:36:26 GMT</pubDate><content:encoded><![CDATA[<p>Most teams have tests. Few have tests that would have caught the bug that just shipped.</p>
<p>The gap is almost always the same one: the test suite exercises the happy path of each endpoint, with a fixture user who is allowed to do the thing. Nobody writes the test where the same request is sent with a token belonging to a different tenant.</p>
<h2>Authorization bugs are invisible to functional tests</h2>
<p>Consider an invoice endpoint. The functional test looks like this:</p>
<pre><code class="language-python">def test_get_invoice(client, user_a, invoice_a):
    resp = client.get(f"/invoices/{invoice_a.id}", headers=auth(user_a))
    assert resp.status_code == 200
    assert resp.json()["id"] == invoice_a.id
</code></pre>
<p>This test passes whether or not the handler checks ownership. It passes when the handler does <code>Invoice.objects.get(id=invoice_id)</code> and returns whatever it finds. The only thing it proves is that an authenticated user can read an invoice that belongs to them — which is not the property anyone cares about.</p>
<p>The property that matters is: a user cannot read an invoice that belongs to someone else. That is a different test, and it is the one that is usually missing.</p>
<pre><code class="language-python">def test_get_invoice_cross_tenant_denied(client, user_a, invoice_b):
    resp = client.get(f"/invoices/{invoice_b.id}", headers=auth(user_a))
    assert resp.status_code in (403, 404), resp.text
</code></pre>
<p>Two things about this test are worth noting. First, it asserts on the <em>negative</em> case, which is the only case that encodes a security requirement. Second, it uses a fixture (<code>invoice_b</code>) that belongs to a different principal — so the fixture setup itself has to model tenancy, not just existence.</p>
<h2>The test is the finding, restated</h2>
<p>If your pentest report says "IDOR on GET /invoices/{id}" and nothing else, you cannot write the test above, because you do not know which IDs, which headers, or which response shape triggered it. You know a category, not an instance.</p>
<p>This is the practical argument for findings that ship with the reproducing request. A finding that arrives as a raw HTTP request can be converted into a test almost mechanically:</p>
<pre><code class="language-plaintext">GET /invoices/8842 HTTP/1.1
Host: app.example.com
Authorization: Bearer &lt;token-of-user-a&gt;
</code></pre>
<p>becomes a fixture, a request, and an assertion. The assertion is not "the scanner said this was bad" — it is "this response must not contain another tenant's data." That is a statement about your system, and it stays true or false independently of any tool.</p>
<h2>Make the fixture adversarial, not convenient</h2>
<p>The most common reason these tests do not exist is that fixtures are built for convenience. <code>user_a</code> owns <code>invoice_a</code>; the factory creates what the test needs and nothing else.</p>
<p>A fixture set that can express cross-tenant access needs at least two principals, each with resources, plus an unauthenticated client. Once that exists, writing the negative test is a two-line change instead of a refactor, and the test gets written.</p>
<p>Keep the fixture data boring and deterministic: two tenants, one invoice each, one admin. The goal is a shape that makes the wrong answer loud, not realistic data.</p>
<h2>Put the negative tests where the regression would happen</h2>
<p>A negative authorization test that runs only in a nightly suite is a test that tells you about the bug twelve hours late. Put it in the same suite as the handler's functional tests, so a change to the handler fails fast.</p>
<p>One caveat: do not assert on error <em>messages</em> or exact status codes across the board. <code>403</code> vs <code>404</code> is a policy decision, and both are acceptable denials. Assert on the property — the response does not contain the other tenant's identifier, or the object count did not change — not on the phrasing.</p>
<h2>What this changes about buying a pentest</h2>
<p>If you evaluate testing tools by "how many findings did it produce," you will get tools optimised for producing findings. If you evaluate by "how many of these can I turn into a failing test in under ten minutes," you get something different: a supply of reproducible requests that slot into the suite you already run.</p>
<p>At HACKBOX PTE. LTD., a Singapore-based company, we run an AI-driven automated penetration testing platform as SaaS, with flexible pricing on a results-based or subscription model. The design premise is the one above — every finding arrives with the request that produced it, ranked by what an attacker could actually reach — because a finding you cannot replay is a finding you cannot regress against.</p>
<p>More at <a href="https://www.hackbox.ai">https://www.hackbox.ai</a></p>
]]></content:encoded></item></channel></rss>