Open-source project
pis10/TraceSurface avatar
pis10/TraceSurface

TraceSurface recovered 135 APIs in one scan, and 124 of them come from a single inference tier

Finds APIs hidden in front-end code and checks for unauthorized access risks, combining dynamic browser tracing with JavaScript static analysis.

501 stars21 forksPythonMIT

At a glance

What is it?
A tool that pairs browser traffic capture with JavaScript parsing to reconstruct an application's API surface, then replays requests without credentials to see which endpoints answer an unauthenticated caller. The methodology is the best part: an explicit evidence tier on every finding and a documented rule that a 200 response is not a success. The demo numbers show how much of the headline result rests on one inference step.
Who is it for?
Use this for API asset inventory on a system you are authorised to test, since the evidence tiers make its output auditable rather than a guess list, and the no-auth replay is the right primitive for spotting endpoints that answer strangers. Do not read its demo headline as a hit rate, because most recovered endpoints are one base-URL concatenation and most successful replays are application-level failures wearing a 200.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 35 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The 135 recovered endpoints are one base URL plus 124 path strings

The worked example scans a login page without authenticating, and the metrics table is worth reading as proportions rather than totals.

316 parsing call sites were found before deduplication, and 135 APIs were recovered from them. That is 42.7 percent, which is a reasonable extraction rate and says nothing surprising.

The interesting column is the tier breakdown: one runtime, one full URL, 124 at tier L1, and three each at L2, L3 and L4.

Tier L1 is defined as a path that uniquely binds to one known base. So 124 of the 135, or 91.9 percent, are the same inference: take a path-shaped string from the source and append the base URL that one runtime request revealed. The three higher-effort tiers, which reason about client relationships, run-wide bases and origin fallback, contribute nine endpoints between them, 6.7 percent.

That is a fair characterisation of what the tool does, and the tier labels exist precisely so a reader can make this calculation. The framing in the example is that one captcha request yielded 135 APIs, and that is true in the sense that one request supplied the base that made 124 others assemblable. It is not 135 independent discoveries.

The README does not show a count for the final tier, the one for call sites where no complete URL could be assembled. Either none occurred on this target or they are excluded from the 135, and nothing says which.

One more number with no context: the sensitive-information hit count is exactly one, with the finding itself not reproduced.

79 of the 88 successful replays were application-level failures

The replay section sends requests with the URL, method, body and content type preserved, and with the browser's cookie and authorization headers stripped. The intent is clean: you see what an unauthenticated caller gets.

The metrics table records 92 replay requests: 88 answered with a 2xx status and 4 with a 4xx. Read alone, that is a 95.7 percent success rate.

Then a note underneath, and it is the most valuable line in the documentation. Of those 88 responses carrying a 2xx status, 79 returned an application-level 401 code in the response body.

So the HTTP-level success rate and the business-level success rate are different numbers by a factor of seven. Thirteen of the 92 replays actually succeeded. The other 79 returned a success status code wrapped around a refusal.

The README turns this into an explicit instruction, and it is the correct one: a 2xx status is not business success, many interfaces answer unauthorised callers with 200 and put the 401 in the body, and you should combine the response content with the permission boundary before drawing a conclusion.

This is worth pausing on, because it cuts against the tool's own output. A scan that reports 88 successes and a rule that says most of those were failures is a tool telling you not to trust its summary number, and that is rarer than it should be. Anyone automating on the status code alone would produce a false-positive report on this very target.

The evidence chain is a positional join on script URL, line and column

The alignment mechanism is the technical core, and it is described precisely.

Every browser request carries the script, line and column that issued it. Every call site found by parsing carries a source span. When the coordinates line up, the real request and the source call site are confirmed as the same API evidence, and the request can then supply a base URL for other call sites.

So the join key is three positional values and no content comparison. There is no check that the string at that line and column is the same string the browser requested.

That assumption is load-bearing and it is fragile in a specific way. It holds when the script the browser fetched is byte-identical to the script the crawler downloaded. It fails when a build step runs between serving and fetching, when a content hash or query string differs between the two, when a server returns a minified bundle for the browser and an unminified one for a plain request, or when a single-file build inlines modules so the line numbers no longer correspond. In each case the alignment misses, and it misses silently: the request simply stops being labelled runtime, and the call sites fall to lower tiers.

There is a detail in the worked example that shows the join is looser than the diagram implies. The runtime request was for a captcha image path with a production API prefix. It was aligned to a call site in the application bundle that contains only the unprefixed path. So what actually matched was a suffix, and the base was inferred from the difference between the full request and the matched fragment.

That is a sensible engineering trick and a different operation from the coordinate precision the documentation describes. Line and column narrow the candidate set; the string comparison does the real work. Worth knowing before you rely on the runtime tier.

The evidence taxonomy has seven outcomes and none of them is a negative

Every recovered endpoint is placed on one evidence axis, and the tiers are defined by where the URL came from and how much confidence that carries.

Runtime means the browser actually made the request. Full URL means the source already contained a complete address, so nothing was assembled. L1 is a path that uniquely binds to one known base. L2 is bound through a client relationship or fan-out. L3 uses a global base discovered during the run. L4 falls back to the target origin. And no-url covers a call site where a complete URL could not be assembled.

Six positive tiers plus a null tier. What is missing is any way to say that a candidate is not an API.

L4 is where this bites. Its definition is origin fallback, which means an endpoint attributed to L4 was assembled by prefixing the target's own origin to a string that looked like a path. A string that looks like a path but is not one, a relative asset reference, a route fragment, a locale segment, a template placeholder, any of these would arrive at L4 looking identical to a real endpoint. The tool has no way to reject them, and a reader of the report has no way to tell which L4 entries are real.

The same applies to L1, one tier up. Uniquely binding to a known base sounds like a strong constraint, and it is a real one, but it constrains the prefix rather than the path. A path that is genuinely not an endpoint still binds uniquely.

So the honest reading of the 135 is 135 candidates, each with a stated confidence level, and the report's job is to let a human triage them. That is a reasonable design for asset inventory. It is not a vulnerability count, and the tier labels are what keep it from being mistaken for one.

Replays strip credentials while the browser session loads them by default

Two independent knobs control authentication, and they point in opposite directions.

The replay layer always strips the cookie and authorization headers. There is no flag to keep them. That is a deliberate design decision and it is what makes the unauthenticated replay meaningful.

The browser session layer does the opposite by default. There is a login command that saves the browser's storage state and optionally the session storage to a file under the tool's home directory, and subsequent scans load it automatically. A separate flag forces scans to run without it.

So the polarity is: replay is always unauthenticated, scanning is authenticated unless you opt out.

The README explains both behaviours and does not draw the interaction together, which is where the surprise is. A scan against a target where someone has run the login command is browsing the authenticated application, so it will discover and render post-login endpoints that an anonymous visitor would never trigger, and then it will replay every one of them with the credentials stripped. That combination is arguably the most useful mode the tool has, because it finds the authenticated surface and tests it anonymously. It is also not what the individual flag descriptions suggest.

The saved session is the other thing to note. It sits in the tool's home directory alongside a database of scan results, a directory of captured response bodies, a directory of fetched source files and a log directory. A stored login session in the same directory as other people's captured data, with no stated file permissions and no cleanup command, is the storage question to ask before running this repeatedly.

The home directory is overridable through an environment variable, which is the right escape hatch if you want that data somewhere you control.

POST is inside the default, and the destructive gate starts at PUT

The safety model is explicit and worth quoting closely.

By default the scan actively sends GET and POST requests. Other methods require an explicit destructive-permitting flag. And there is a discovery-only flag that skips replay entirely.

So the boundary is drawn between POST and PUT, with POST treated as non-destructive.

That is a judgement call, and it is arguable in both directions. POST is the method most frameworks use for creating a record, submitting a form, sending a message, or placing an order, so in application-level terms it is frequently state-changing. Against that, the replays run without credentials, so a request that would create something is generally rejected before it acts.

The second consideration is what the requests are. They are derived from the application's own frontend code, so the URL, method, body and content type are whatever a real user action would send. If the target has any endpoint that mutates state without requiring authentication, or where authentication is checked after the write, then a default scan is issuing real state-changing traffic against an authorised target during what looks like a read-only inventory pass.

That is a genuine reason to use the discovery-only flag on a first run against a production system, and the README does recommend the flag for cases where you do not need verification.

The authorisation statement itself is brief and to the point: only use it on targets you own or have explicit permission to test. For a tool whose default mode sends live traffic, that sentence carries more weight than its length suggests, and it is the first thing a reader should confirm before anything else.

The source install never mentions the frontend build the wheel depends on

The from-source path is three commands and one requirement:

bash
git clone https://github.com/pis10/TraceSurface.git
cd TraceSurface

uv sync

Python 3.12 and that package manager. Then the tool runs through the module runner.

Now look at what the build configuration declares. The wheel's only declared artifact is a static directory inside the server package, and the source distribution force-includes the same path. That path is the compiled report frontend.

So the Python distribution needs built frontend assets, and the repository layout confirms the two halves are separate: a Python package directory, a frontend directory, and a scripts directory at the top level.

None of the from-source instructions mention building the frontend. There is no frontend build step between cloning and running, and no note saying the report server will start without its assets.

That produces a specific and confusing failure mode for anyone building from source rather than downloading a release archive. The install succeeds, the scanner works, and the local report is the one feature that comes up empty. And the release path masks the problem entirely, because the README's primary distribution channel is a per-platform archive from the releases page, where the assets are already built.

The interpreter requirement is a smaller version of the same gap. The manifest requires 3.12 or newer, the classifier advertises only 3.12, and the type checker is configured against 3.12 specifically. So the declared support range is wider than the declared support classification.

A type checker with eight any-suppressions and no test directory in the tree

Two tooling observations, one of which is a deliberate choice you may or may not agree with.

The linter dependency group contains exactly one tool, a type checker, at version 1.29 or newer. It runs in a mode called standard rather than strict. And then fourteen individual diagnostics are configured, of which two are escalated to warnings and the rest are switched off entirely.

The switched-off ones are the type-safety diagnostics: explicit any, implicit override, and then eight separate reports covering unknown variable types, unknown member types, unknown argument types, unknown parameter types, unknown lambda types, and the catch-all for explicit any. Import-cycle reporting and missing type arguments are left as warnings.

For most projects that is a sensible starting point. For this one it is worth naming, because the project's entire claim is that every finding carries a stated evidence tier, and the code implementing that tiering has its unknown-type detection turned off. The claim is about the output being auditable, and this is the configuration of the code that produces the output.

The second observation is simpler. The repository has no test directory. The top level holds a version pin file, a lockfile, the two readmes, the licence, documentation, the frontend, the Python package, a scripts directory, an editor configuration and a continuous integration directory. No tests.

There is also no test dependency group. The only group declared is the linter, and the only console script is the scanner itself. So there is no test command to run from the documentation, which is consistent: the README documents scanning and serving and says nothing about verifying the code.

The presence of a continuous integration directory means something is running automatically, and the type checker being the sole declared tool suggests that is what it runs.

Editorial conclusion

Use this for API asset inventory on a system you are authorised to test, since the evidence tiers make its output auditable rather than a guess list, and the no-auth replay is the right primitive for spotting endpoints that answer strangers. Do not read its demo headline as a hit rate, because most recovered endpoints are one base-URL concatenation and most successful replays are application-level failures wearing a 200. Verify first whether your target serves minified bundles from a different pipeline than the crawler fetches, since the whole evidence chain joins on script URL plus line and column.

Frequently asked questions

What does TraceSurface do and how does it find APIs?

It starts from a URL, captures real browser traffic while parsing the JavaScript, and joins the two on script URL plus line and column. The single runtime request supplies a base URL, which is then used to assemble endpoints the page never called, including ones inside lazily loaded bundles. It also reconstructs webpack or Vite chunk mappings and visits discovered frontend routes rather than waiting for a user to click through them.

What are the evidence tiers in TraceSurface results?

Runtime means the browser actually made the request. Full URL means the source already held a complete address. L1 is a path uniquely bound to one known base, L2 is bound through a client relationship or fan-out, L3 uses a base discovered during the run, L4 falls back to the target origin, and no-url marks a call site where no complete URL could be assembled. There is no tier for rejecting a candidate as not an API.

Does a successful TraceSurface replay mean the endpoint is accessible?

Not on its own. In the tool's own worked example, 88 of 92 replays returned a 2xx status, and 79 of those returned an application-level 401 code in the response body. The documentation states plainly that a 2xx status is not business success and that many interfaces answer unauthorised callers with 200, so you should read the response content and the permission boundary together.

How does TraceSurface handle authentication?

Replays always strip the browser's cookie and authorization headers, so every replay is an unauthenticated viewpoint. Separately, a login command saves the browser storage state and optional session storage under the tool's home directory, and later scans load it automatically unless a flag forces them not to. So replay is always unauthenticated while the browsing session is authenticated by default.

What are TraceSurface's safety controls and its default traffic?

A scan actively sends GET and POST requests by default, other methods require an explicit destructive-permitting flag, and a discovery-only flag skips replay altogether. The authorisation statement is that the tool should only be used against targets you own or have explicit permission to test, which matters because the default mode sends live traffic derived from the target's own frontend code.

Official sources

  1. Issues
  2. License: MIT
  3. pis10/TraceSurface on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pis10-tracesurface.svg)](https://hysenlabs.com/projects/pis10-tracesurface)