Skip to content

feat: refang defanged IoC inputs, with a global --no-refang opt-out - #274

Merged
vhmartinezm merged 11 commits into
developfrom
ioc-refang
Sep 23, 2026
Merged

vhmartinezm merged 11 commits into
developfrom
ioc-refang

Conversation

@vhmartinezm

@vhmartinezm vhmartinezm commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

What

The CLI now refangs defanged indicators of compromise. A URL, domain or IP pasted straight out of a threat-intel report (hxxps[:]//evil[.]com, 127[.]0[.]0[.]1, evil[dot]com) is sent in its live form. A new root option, --refang/--no-refang (on by default), controls it.

Why

Reports print indicators defanged so that nobody clicks one by accident. Pasted as-is, a defanged value never matched a search. scan url and sandbox url rejected it as an invalid URL.

How

The SDK can now refang the URL, domain and IP inputs of its own endpoint methods. On the SDK that is opt-in (refang_iocs defaults to False, which keeps its release a minor bump), and the CLI opts in: --refang/--no-refang is passed to it as refang_iocs, on unless --no-refang is given. So these commands need no CLI code:

  • search url
  • search metadata -p/-u/-d (the free-form query is never changed)
  • search ioc ip|domain
  • search known
  • known add / known update
  • scan url, including -r/--url-file lines

The CLI handles the value itself in two places, and applies the SDK's refang_ioc there through utils.refang_input, with the same switch:

  • Validation that runs before the SDK sees the value. scan url and sandbox url now validate the refanged form, so a defanged URL is judged as it will actually be submitted.
  • A request the CLI builds itself. metadata analyze-ip goes through Polyswarm.submit_url, which uses _single rather than an SDK endpoint method.

Hashes and ids are never touched, and neither is a --qrcode-file path: that exemption lives in the SDK (a QR-code submission skips refanging entirely), and two tests here pin it for scan url and sandbox url, including the url=None that sandbox url --qrcode-file hands the SDK. With --no-refang, every input is sent verbatim and the old validation applies unchanged.

Behaviour change (for the release notes)

scan url and sandbox url now accept a defanged URL such as hxxps[:]//evil[.]com and submit https://evil.com, where they used to reject it with "URL … is not valid". Searches that used to find nothing for a defanged value now find the live one. The write paths change too: known add / known update now store the live host, so known-host rows an earlier CLI wrote verbatim as evil[.]com are no longer matched by search known -d evil[.]com (which now sends evil.com), and a defanged scan url / sandbox url submits a different artifact (content, name, sha, quota) than the same invocation did before. The read side cuts both ways: scan url -r urls.txt never validated its file lines, so an earlier CLI could store a genuinely defanged URL artifact, and search url hxxps[:]//evil[.]com now refangs and no longer reaches it. Pass --no-refang for the previous behaviour, including to reach catalogue rows or URL artifacts stored defanged. There is no CHANGELOG in this repo, so the develop → master release PR should list this.

SDK floor: 4.6.0

This change raises the floor to polyswarm_api>=4.6.0, the SDK version that introduces polyswarm_api.refang and the refang_iocs= keyword. On 4.5.0 the constructor rejects that keyword. The SDK branch of the same name declares 4.6.0, and CI installs it from that branch. Merge the SDK PR first, and release the SDK before this repo's release.

Tests

tests/refang_test.py (25 tests) mocks at the SDK transport, PolyswarmSession.execute (the documented session customization point; new testing Style 4 in specs/04-testing.md), and reads the built request's params and JSON body. A mock on search_url and the other endpoint methods would prove nothing, because the refang runs inside them. The tests cover each command above with the flag on and off, live input left unchanged, the --no-refang rejection in scan url and sandbox url, the validation error quoting the typed (not the rewritten) URL, and the refang note in the --help of scan url, sandbox url, known add and known update.

Full suite: 221 passed.

Requires

Pasting an indicator straight out of a threat-intel report
(hxxps[:]//evil[.]com, 127[.]0[.]0[.]1) never matched a search, and
scan url / sandbox url rejected it as an invalid URL.

The SDK now refangs the URL/domain/IP inputs of its endpoint methods;
the new root --refang/--no-refang flag (default on) is passed through as
refang_iocs. Where the CLI handles the value itself - the is_url
validation in scan url and sandbox url, and the CLI-owned submit_url
request behind metadata analyze-ip - it applies the SDK's refang_ioc via
utils.refang_input, so a defanged URL is validated in the form that is
actually submitted.

Raise the SDK floor to 4.6.0, which introduces the surface.
Capture the built request at PolyswarmSession.execute, the documented
session customization point, instead of patching _paginate/_single and
rebuilding the request with _to_request. Add the --no-refang rejection
case for sandbox url and the known add/update host cases.
@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/01, 02, 04, 05. Gitflow (base develop, no CLI version bump), the floor bump to 4.6.0 with its rationale in 05-sdk-contract.md, the ## Requires link, and the new Style 4 in 04-testing.md with its test-only PolyswarmSession.execute dependency entry all check out. Two things worth acting on, both on the QR-code paths the PR declares out of scope:

1. sandbox url --qrcode-file now feeds url=None into an SDK method that refangs its URL argument — and nothing tests it.

client/sandbox.py:133 calls api.sandbox_url(url, …) with url=None on the qrcode branch. Per this PR, 4.6.0's sandbox_url refangs its URL input. Nothing in this repo pins that the SDK's refang is None-safe, and grep -rn qrcode tests/ returns exactly one hit — refang_test.py:127, the --url-file case. There is no qrcode test in the suite at all, here or in preprocessing_test.py. If refang_ioc does not guard None, sandbox url --qrcode-file becomes a TypeError and CI here stays green. Add the case to tests/refang_test.py (Style 4: assert the request body still carries the artifact, with refang on).

2. specs/02-commands.md — "Hashes, ids and QR-code files are never touched" is not enforced by CLI code on the scan url side.

client/scan.py:99 sets urls = [qrcode_file], and Polyswarm.scan_url (src/polyswarm/polyswarm.py:94-99) submits every element as submit(<value>, artifact_type="url", …) — the same call test_scan_url_file_lines_are_refanged uses to prove the SDK refangs url-typed submissions. So the QR-code file path goes through the SDK refang like any other url input; the exemption, if it holds, lives entirely in the SDK's gate, not in anything this PR adds. Either pin it (a qrcode test asserting the path reaches the wire byte-identical) or narrow the sentence to say it is the SDK's already-live/not-defanged gate that exempts it, not CLI code.

Everything else matches the documented conventions: refang_input gating on the public api.refang_iocs, the refang-before-validate ordering in scan url / sandbox url, the explicit refang in the CLI-owned submit_url, and the spec updates in 01/02/04/05.

@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Addressed in 65b10ce:

  1. sandbox url --qrcode-file: new test pins that the url=None QR branch reaches the transport with the image path unchanged. A mutation check confirmed it catches the regression you describe: hoisting the SDK's refang above the QR branch while dropping its non-string guard turns it red.
  2. scan url --qrcode-file: new test pins that a qr[.]png path reaches the wire byte-identical (it is red if the SDK refangs the QR branch). specs/02-commands.md now says the exemption lives in the SDK, not in CLI code.

The SDK side of this pair also tightened its gate (a rewrite that only changes a URL's path is dropped); the CLI suite passes against it: 219 passed.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/01, 02, 04, 05.

Correctness, downstream contract, testing and gitflow all look right:

  • scan url / sandbox url refang before is_url, and both hoist api = ctx.obj[api] above the branch so the qrcode path is still reached first. The --qrcode-file argument never goes through refang_input, and the two qrcode tests pin the SDK-side exemption.
  • Polyswarm.submit_url is the only CLI-owned _single request in the tree, so refanging it there fully covers the "CLI builds the request itself" case. Everything else (search url via the search_urls fan-out, search metadata, search ioc, search known, known add/update, scan url including --url-file) reaches an SDK endpoint method, so "no CLI code" is correct.
  • Style 4 is justified: a PolyswarmAPI.search_url mock would replace the code under test. The new test-only seam (PolyswarmSession.execute, request.params / request.input_json) is recorded in 05-sdk-contract.md as that spec requires.
  • Floor bump to 4.6.0 follows 05-sdk-contract.md (Raising the floor): SDK PR linked under Requires, no CLI version bump, base is develop, branch name shared with the SDK branch so the CI_COMMIT_BRANCH archive resolves. The behaviour change is called out for the develop-to-master release notes, which is what the spec asks for given there is no CHANGELOG.

One thing to fix — specs/01-architecture.md:85 (Support, utils.py) is now incomplete.

That bullet enumerates the module surface: "parallelize/parallel_executor ... plus collect_files and the detection helpers (is_valid_id, is_ip, is_domain, is_url)". refang_input is a new public helper in that module and is not listed. It is documented in 02-commands.md and 05-sdk-contract.md, but AGENTS.md ("Update the spec in the same PR as the code change") makes the file-role list in 01 where a reader looks for what lives in utils.py. One added clause closes it — worth noting there that it is gated on the client refang_iocs, so it reads as a refang helper rather than another detection helper.

Nothing else blocking.

@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Addressed in 8b4fb24: specs/01-architecture.md now lists refang_input in the utils.py bullet as the refang helper gated on the client's refang_iocs. Also, following the SDK review, the SDK's refanging is now opt-in (default False). The CLI already passes refang_iocs explicitly from --refang/--no-refang, so its behaviour is unchanged. specs/02-commands.md and the PR body now say so. 219 passed against the updated SDK branch.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Review — checked against AGENTS.md, specs/01-architecture.md, 02-commands.md, 04-testing.md, 05-sdk-contract.md, and the companion SDK PR (polyswarm/polyswarm-api#327).

Verified clean:

  • Correctness. refang_ioc is idempotent (candidate == trimmed -> unchanged), so the CLI refanging positional URLs before is_url and the SDK refanging the same value again inside submit / sandbox_url is a no-op on the second pass. Every CLI site that handles an IoC outside an SDK endpoint method is covered: the only two is_url call sites (client/scan.py:110, client/sandbox.py:130) and the only _single request the CLI builds itself (Polyswarm.submit_url, polyswarm.py:289). search_urls / scan_url / sandbox_instances are fan-out wrappers over SDK endpoint methods, so the SDK refang reaches --url-file lines too. sandbox url --qrcode-file hands url=None to sandbox_url; refang_ioc passes non-strings through, and the qrcode exemption is on the SDK side as claimed.
  • Spec drift. 01-architecture.md (global options, utils.py, lifecycle), 02-commands.md (the new global-flag section), 04-testing.md (Style 4) and 05-sdk-contract.md (public-surface table row, test-only PolyswarmSession.execute seam, floor rationale) are all updated in the same PR, as AGENTS.md requires.
  • Floor bump. Follows 05-sdk-contract.md §Version pin exactly: the SDK ioc-refang branch declares 4.6.0 in both pyproject.toml and __init__.py with no pre-release suffix, so the branch archive CI installs satisfies >=4.6.0 and will not be silently replaced from PyPI. Branch name is identical across both repos (the $CI_COMMIT_BRANCH key in .gitlab-ci.yml) and is descriptive rather than ticket-prefixed.
  • Gitflow. Base is develop; pyproject.toml version is untouched (only the dependency floor moved), which is what AGENTS.md asks for in a feature PR; ## Requires links the SDK PR; no ticket IDs, no AI-attribution trailers.

One thing to fix — specs/02-commands.md line 67 omits the write-path consequence of defaulting the switch on.

The new section frames refanging entirely as a read-path fix: "pasted verbatim they never match a search, and a submitted one becomes a broken URL artifact". But the SDK's own contract spec is explicit that turning it on changes what the write paths persist, and this PR turns it on by default for every CLI user:

the default matters beyond searches: turning refanging on changes what the write paths store — a known-host row written verbatim as evil[.]com by an earlier client is no longer matched by a lookup that now sends evil.com, and a URL submission creates a different artifact (content, name, sha) than the same defanged call did before.
— polyswarm-api specs/05-downstream-contract.md, §IoC refanging

Concretely, with no flag given:

  • known add domain evil[.]com feed now stores evil.com. Rows an earlier CLI wrote verbatim as evil[.]com are no longer reachable from search known -d evil[.]com, which now sends evil.com. The two become silently disjoint sets and nothing in the CLI surfaces that.
  • scan url hxxps[:]//evil[.]com submits a different artifact (different content, artifact_name and sha) than the same invocation did on 4.5.0, and bills quota against it.

The PR body's "Behaviour change (for the release notes)" section has the same gap — it lists the scan url / sandbox url acceptance change and the search change, but not the write paths. Since there is no CHANGELOG here, 02-commands.md is the durable place this has to live, and it is what the develop → master PR will be written from. Add a sentence there covering both write cases and pointing at --no-refang as the escape hatch, and mirror it in the PR body so it survives into the release notes.

@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Addressed in 465064c. specs/02-commands.md now has a "Write paths change too" paragraph covering both cases: known add/update store the live host, so rows written defanged before are no longer matched by a defanged search known; and a defanged scan url/sandbox url submits a different artifact (content, name, sha, quota). It points at --no-refang as the escape hatch. The release-notes section of the PR body mirrors it.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/01, 02, 04, 05. Correctness, SDK contract, test layering, floor bump, and gitflow (base develop, no CLI version bump, ## Requires present, shared branch name, clean commit subjects) all check out.

One nit — stale doc line the PR half-updated:

  • AGENTS.md:91 still reads "Two styles for that: SDK-boundary mocks … and VCR cassettes", but the new bullet on the next line adds a third command-behaviour mocking style. specs/04-testing.md had the parallel sentence fixed in this PR ("the mocking styles (SDK-boundary mocks, SDK-transport mocks, VCR cassettes)"); AGENTS.md should match — s/Two styles/Three styles/ and name the transport mock inline, or drop the count.

Two things I checked and am satisfied with, noted only so the next reader does not re-derive them:

  • scan url (and sandbox url) refang in the CLI and then hand the already-refanged value to an SDK endpoint method that refangs again. That is safe only because the SDK gate leaves live input alone, which test_search_url_live_value_is_unchanged pins (https://example.com/a[.]b unchanged) — i.e. the idempotence is pinned on the SDK side of the seam, not by accident.
  • refang_input reading api.refang_iocs unguarded is correct per specs/05-sdk-contract.md §"Raising the floor is the whole procedure" — no getattr, the floor is the guard.

@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Fixed in 8c41887: AGENTS.md now says "Three styles" and names the SDK-transport mock inline, matching specs/04-testing.md.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/01, 02, 04, 05 (and cross-checked the contract claims against the companion SDK PR). Clean — no blocking findings.

Verified:

  • Refang call sites are complete. is_url appears only in client/scan.py:110 and client/sandbox.py:130, and submit_url (polyswarm.py:291) is the only CLI-owned _single request carrying a URL/IP — exactly the two places the PR touches. Every other IoC-bearing command (search url|metadata|ioc|known, known add/update, scan url -r) reaches the wire through an SDK endpoint method, which matches the SDK PR's list of refanged inputs.
  • Floor bump follows 05-sdk-contract.md §Version pin. pyproject.toml is the only place the SDK version is expressed; the SDK branch of the same name declares a clean 4.6.0 with no dev suffix, so the archive satisfies the floor. §Current floor header and rationale updated in the same PR, as the spec requires.
  • Gitflow. Base is develop, branch name is descriptive rather than ticket-prefixed, src/polyswarm/__init__.py is untouched (no release fired), and the SDK PR is linked under ## Requires.
  • No downstream break for library consumers. The SDK keyword defaults to False, so a bare Polyswarm('key') (README §Using as a package) keeps 4.5.0 behaviour; only the CLI opts in. tests/metadata_test.py::test_analyze_ip_submits_url still exercises the real submit_url body through the new refang_input call with a live value.
  • Style 4 is justified and documented. A PolyswarmAPI.search_url mock would replace the code under test, so the transport seam is the right layer; the test-only dependency on PolyswarmSession.execute / request.params / request.input_json is recorded in 05-sdk-contract.md rather than left implicit, and 04-testing.md's "mock at exactly one point per path" invariant was widened to match.

One non-blocking maintainability note, src/polyswarm/client/scan.py:104:

positional = [refang_input(api, u) for u in url]
urls = list(positional)

The list(...) copy now looks redundant (positional is already a list) but is load-bearing: url used to be a tuple, so urls.extend(url_file…) could not touch it. Collapsing this to urls = positional would alias the two, and the for _url in positional validation loop below would silently start rejecting --url-file lines, which have never been validated. A short comment on the copy would stop a future simplification from reintroducing that.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Review

Correctness, SDK contract, gitflow and test coverage all check out. One spec-drift item, one nit.

1. Behaviour change is missing from the affected commands' own --help (spec drift)

specs/02-commands.md sets the precedent explicitly, for the last change of exactly this kind:

This repo has no CHANGELOG, so the note lives here and in the command's own --help rather than only in a release-time reminder.

...and client/live.py:60-61 carries that note in the command docstring, not just in the spec.

This PR changes what four unchanged invocations do — scan url and sandbox url submit a different artifact (content, artifact_name, sha, and the quota it costs), and known add / known update store a different host — but nothing in those commands' docstrings or option help says so. The only user-visible mention is the root group's --refang help, which a user running polyswarm scan url --help never sees. The new global-refang section in specs/02-commands.md documents it well; per the repo's own rule, a spec note is not the delivery vehicle for this.

Suggest a line in the scan url, sandbox url, known add and known update docstrings along the lines of: "Defanged URLs/hosts (hxxps[:]//evil[.]com) are refanged before submission; pass --no-refang on the root command to send them verbatim."

(The release-notes half is handled — the PR body flags it for the develop -> master PR, which is what specs/05-sdk-contract.md asks for.)

2. Nit: validation errors now quote a string the user never typed

client/scan.py:105-112 and client/sandbox.py:129-132 rebind to the refanged value before building the message, so an input that is defanged and invalid reports URL "<rewritten>" is not valid. Keeping the original for the message text — while still validating the refanged form, as now — makes the error greppable against what was pasted.

Checked and clean

  • Correctness — Polyswarm.__init__ forwards refang_iocs through **kwargs; the falsy guard in sandbox url is right; qrcode branches never reach refang_input; the CLI-then-SDK double refang on scan url / sandbox url is pinned end-to-end by test_scan_url_accepts_and_refangs_a_defanged_url, so idempotence is not taken on faith.
  • Surface coverage — submit_url is the only _single call site in the repo, and metadata.py:43 is the only url/domain/ip argument outside search.py/scan.py/sandbox.py. Nothing missed. notification_webhook's webhook_uri is a distinct kwarg and correctly untouched.
  • SDK contract — floor raised to 4.6.0 with the rationale in the Current floor section, no getattr/probe guards, and the module-level from polyswarm_api import refang fails loudly on an older SDK as the floor policy intends. The test-only PolyswarmSession.execute / request.input_json coupling is declared, and session is already named as a power-user helper in the imports invariant.
  • Tests — Style 4 is the right call (a search_url mock would replace the code under test), and it is documented in specs/04-testing.md before being used. Every command the new spec section claims is covered has both a refang and a --no-refang assertion on the wire.
  • Gitflow — base develop, Requires section present, branch name matches the SDK branch for $CI_COMMIT_BRANCH resolution, and pyproject.toml's version is untouched (only the dependency floor moved, which specs/05-sdk-contract.md calls a normal code change).

🤖 Generated with Claude Code

@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Addressed in 0878582:

  1. scan url, sandbox url, known add and known update now each note in their own --help that defanged input is refanged (submitted or stored in its live form), and that --no-refang on the root command sends it verbatim. The paragraphs are marked \\b, so Click doesn't split the flag name when it rewraps. A test asserts the note is present in all four.
  2. Validation still runs on the refanged form, but the error now quotes what was typed. A test covers fxp://files[.]example[.]org, which refangs to an ftp URL that is_url rejects, and checks the message quotes the original input.

221 passed.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/01, 02, 04, 05. Correctness of the refang wiring checks out: the CLI-side refang is confined to the two places the specs name (pre-SDK is_url validation in scan url / sandbox url, and the _single-built submit_url), refang_ioc is gated + non-string-passthrough in the SDK so the double application on the positional scan/sandbox URLs and the url=None qrcode path are both no-ops, and the --qrcode-file exemption is correctly left to the SDK. Gitflow is clean: base develop, no CLI version bump, ## Requires present, and the SDK branch is ioc-refang on both repos with version = "4.6.0" (no dev suffix) — so the >=4.6.0 floor is mergeable-but-not-releasable exactly as specs/05-sdk-contract.md §Current floor describes.

Three things to action, none blocking:

1. The read side of the catalogue change has no --help note (src/polyswarm/client/search.py:108, src/polyswarm/client/metadata.py:45).
The PR body identifies the sharpest user-visible trap itself: search known -d evil[.]com now sends evil.com and silently stops matching rows an earlier CLI stored defanged. known add / known update got a help note; search known — the command that returns nothing — did not. Same for metadata analyze-ip, whose argument is refanged by Polyswarm.submit_url. tests/refang_test.py:576 pins the note on four commands; the loop should cover search known and metadata analyze-ip too (and arguably search url / search metadata / search ioc, where a defanged value now resolves to a different artifact set). The rationale for in-command help over a spec-only note is already in the test's own comment — apply it to the commands where a user will actually hit it.

2. specs/02-commands.md contradicts itself on scan url.
The section says the SDK refang means … and scan url (including -r/--url-file lines) need no CLI code, and then four lines later lists scan url under "Two places handle the value in the CLI". Both are half-true and the combination is confusing to the next reader. Narrow the first list to the -r/--url-file lines (which genuinely reach the wire only via the SDK's submit) and keep positional URLs in the second.

3. Four commit subjects are truncated mid-word (f6ef323, 65b10ce, 8b4fb24, 08785824).
e.g. docs: document the SDK-transport test style and the refang global opt… with a body of …ion in the specs. AGENTS.md §Commit + PR hygiene asks for small, independently reviewable commits; a subject that continues into the body is neither. A squash-merge with a clean subject contains it, but worth fixing at the source.

One observation, no action needed: Polyswarm used as a library (README §"Using as a package") keeps the SDK default refang_iocs=False, so only CLI invocations change behaviour. That asymmetry looks deliberate and matches the SDK's opt-in design.

🤖 Generated with Claude Code

Also narrow the spec's no-CLI-code list to the --url-file lines of scan url; positional scan/sandbox URLs are refanged in the CLI before validation.
@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Addressed in 60a85b9:

  1. search known (the read side of the catalogue change) and metadata analyze-ip now carry the refang note in their own --help, and the help-note test covers all six commands.
  2. specs/02-commands.md: the no-CLI-code list is narrowed to the -r/--url-file lines of scan url. Positional scan url / sandbox url URLs stay in the list of places where the CLI refangs before validating.
  3. Commit subjects: they are complete in git. They are longer than 72 characters, which GitHub's UI truncates with an ellipsis, and none continues into the body (git log --format=%s). This PR is squash-merged with a clean subject, per AGENTS.md, so none of them reach develop. Newer commits keep subjects short.

This is the last round of changes on this PR. Anything non-blocking that comes up from here on will be answered with a disposition rather than another push.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/01, 02, 04, 05. Clean — no correctness, contract or gitflow issues found.

Checks that mattered:

  • Correctness. refang_input gates on api.refang_iocs and delegates to the SDK's refang_ioc, so the CLI's rewrite can never diverge from the SDK's (the SDK applies it with no accept= either) and the double application on positional scan url / sandbox url URLs is a no-op — refang_ioc returns value unchanged once refang_text(trimmed) == trimmed. refang_ioc passes non-strings through, so sandbox url --qrcode-file's url=None is safe; the if url else url guard in client/sandbox.py:136 is belt-and-braces. Validating the refanged form while quoting typed in the error is the right call, and the qrcode branches never reach refang_input at all.
  • Coverage of IoC-bearing inputs. Every url / ip / domain parameter in client/ is accounted for: search url, search metadata -p/-u/-d, search ioc ip|domain, search known, known add/update via SDK endpoint methods; positional scan url / sandbox url and metadata analyze-ip (the _single request in polyswarm.py:289) in CLI code. grep for _single(/_paginate( in src/ finds no other CLI-owned request. Webhook destination URLs are correctly left alone.
  • Contract / floor. refang_iocs is keyword-only in the SDK 4.6.0 constructor and is passed as a keyword; >=4.6.0 floor matches the SDK branch's declared 4.6.0 (no dev suffix), and specs/05-sdk-contract.md §Current floor moved with the pin. The new polyswarm_api.refang import and the test-only PolyswarmSession.execute / request.params / request.input_json seam are both recorded there; input_json is the right attribute name (the SDK's own request-shape tests read the same field).
  • Tests. Style 4 is justified — a search_url mock would replace the code under test — and autospec=True with fake_execute(session, request) is correct for an unbound patch. _Stop is not an api_exceptions.* type, so utils.parallel_executor does not swallow it on the scan url fan-out.
  • Gitflow. Base develop, pyproject.toml version untouched, SDK PR linked under ## Requires, shared descriptive branch name, no ticket IDs or AI trailers in the commits.

One minor note, no action required before merge:

  • The read-path behaviour change is described as one-directional. The PR body and specs/02-commands.md §Global say searches that found nothing for a defanged value now find the live one, but the converse also holds: scan url -r urls.txt never validated its file lines, so an earlier CLI could submit a genuinely defanged URL artifact, and search url hxxps[:]//evil[.]com now refangs and no longer reaches it — same --no-refang escape hatch as the catalogue rows already called out. Worth a clause there, and it belongs in the develop → master release notes alongside the write-path paragraph. Relatedly, search url / search metadata / search ioc are the affected commands whose --help says nothing about refanging (test_behaviour_change_is_in_each_affected_commands_help deliberately lists only the write and lookup paths) — fine as a choice, but the search read paths are where a user hits the surprise.

@vhmartinezm

Copy link
Copy Markdown
Contributor Author

Thanks. The converse read-path case (a defanged URL artifact stored by an earlier scan url -r is no longer reached by a defanged search url) is now in the release-notes section of the PR body, next to the catalogue rows, with the same --no-refang escape hatch. The --help of the search read paths (search url / search metadata / search ioc) is left as a deliberate choice: the note is on the commands that write or look up catalogue state, and the root --refang/--no-refang help covers the rest. No code change, per the convergence note above.

@mjbradford89

Copy link
Copy Markdown
Contributor

Summary

Lets an analyst paste an indicator straight out of a threat-intel report. A defanged URL, domain or IP (hxxps[:]//evil[.]com, 127[.]0[.]0[.]1) is rewritten to its live form before the request is built, so a search finds the artifact instead of silently missing it and a submission creates the real URL rather than a broken one. A new pure module in the SDK does the rewrite behind a deliberately narrow gate; the SDK keeps it opt-in so 4.6.0 stays a minor bump, and this repo opts in by default behind --refang/--no-refang. 2 PRs on ioc-refang; 26 files, +1032/−31 across the set.

Severity: 0 HIGH · 1 MODERATE · 1 LOW — none of them in this repo.
Prior feedback: all 21 points checked and addressed (2 answered with a disposition that stands).
Objective: met — refang defanged URL/domain/IP inputs before the request is built, measured against the PR bodies.

Fixes are proposed, not applied; nothing was run.

Cross-repo coordination

Member PR State Role
polyswarm-api polyswarm/polyswarm-api#327 OPEN SDK
polyswarm-cli #274 OPEN CLI ← you are here

Merge order: polyswarm-api#327 → polyswarm-cli#274 — this repo's floor names 4.6.0, and only the SDK branch declares it. Merge the SDK PR first, and release the SDK before this repo's release, exactly as the PR body says.

Contracts crossing the set: none carries a finding. refang_iocs=, polyswarm_api.refang.refang_ioc and the public api.refang_iocs attribute this repo reads through utils.refang_input all match the SDK branch.

Coherence: fixes are independent; no cross-repo adjustment needed, and nothing proposed for the SDK changes a surface this repo consumes.

Worth knowing: a third client — the web UI — implements the same refang contract on its own branch, with a byte-identical refang_cases.json and the same pinned digest. So a refang rule change is a three-repo change, and no branch-name scan starting from these two PRs will surface it.

Findings (round 1)

No defects found in this repo's diff. Every refang entry point is accounted for: the SDK refangs the inputs of its own endpoint methods, and the two places this repo handles a value itself — the pre-SDK is_url validation in scan url / sandbox url, and the CLI-owned submit_url request — both go through utils.refang_input gated on api.refang_iocs. The double application on positional scan url / sandbox url values is a genuine no-op: refang_ioc returns its input unchanged once the rewrite is a fixed point, which I re-derived by executing the module against the shared case table rather than taking it on trust.

elsewhere: F1, F2 → polyswarm/polyswarm-api#327

Standards conformity

Change-level: no violations introduced by this diff.

Project-level, non-clean rows:

  • polyswarm-cli §2 (shared GitLab CI template) ◐ — no e2e: job. .gitlab-ci.yml includes the shared template and has dev-pypi, three pytest jobs and release-pypi, but nothing extending .e2e (the SHOULD criterion; the absent .build-docker/.release-docker jobs are legitimate for a PyPI-only package). Pre-existing and untouched by this PR — flagged because it is why the half of this capability that ships refanging on by default has no pipeline tier that runs against a stack. Not a finding against this change.

polyswarm-cli — Clean: §14 (delivery order), §15 (comment the fact, specify the design), §16 (cross-repo version pin: the floor covers every surface used, no getattr probes or shape-keyed test skips, release order stated in the PR body and the spec). Not applicable: §3–§13, §17.

Set-level: §14 clean — one externally-facing capability rather than a layer, and no UI change merging ahead of it. Branch-name identity clean: ioc-refang is byte-identical and tag-safe across both members whose CI resolves companions by name. ## Requires clean — this PR links the SDK PR it depends on.

@mjbradford89 mjbradford89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. No findings in this repo. Merge after polyswarm/polyswarm-api#327, which the 4.6.0 floor here depends on.

@vhmartinezm
vhmartinezm merged commit 2fbb464 into develop Sep 23, 2026
2 checks passed
@vhmartinezm
vhmartinezm deleted the ioc-refang branch September 23, 2026 17:39
@sbneto

sbneto commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets an analyst paste an indicator straight from a threat-intel report (hxxps[:]//evil[.]com,
127[.]0[.]0[.]1) and have every PolySwarm client use its live form. Searches stop silently
missing, and submissions stop creating broken URL artifacts. One gated rewrite ships in three
clients, driven by one byte-identical case table. They are the SDK (opt-in, the reference), the CLI
(on by default) and the web UI's search bars and upload card. 3 PRs on two branch names; 40 files,
+1733/−34 across the set; SDK and CLI already merged.

Severity: 0 HIGH · 2 MODERATE · 4 LOW — none of them in this repo. Prior feedback: 23 checked · 2 open.
Objective: met, with gaps — let analysts paste defanged IoCs into search without re-fanging them by hand.

  • missing: the web UI's header Search page still searches defanged values verbatim → F1
  • drift: the web UI's upload card refangs too; not asked for, works

Fixes are proposed, not applied; nothing was run.

Cross-repo coordination

Set: polyswarm-api#327 merged · SDK (reference) — polyswarm-cli#274 merged · CLI ← you are here — the web UI's PR (private repo) open · UI

Merge order: polyswarm-api#327 → polyswarm-cli#274 → the web UI's PR, already honoured (§14: SDK and CLI before UI).
The SDK fixes (F2, F3, the SDK half of F4) need a new PR against develop before 4.6.0 is released. Nothing here changes: this repo never calls sandbox_file for QR codes, and its >=4.6.0 floor stands.

Surface Producer Consumer
refang_cases.json + rule set (standalone run: both implementations agree on all 250,042 generated inputs) polyswarm-api the web UI → F4, F6

Coherence: the fixes are independent across repos, and none touches a surface this repo consumes.

Findings (round 2)

No defects found in this repo's diff. This CLI still refangs search ioc and search url by default, which is the behaviour F1 asks the web UI's header Search page to match.

elsewhere: F2, F3 → polyswarm-api#327 · F1, F5, F6 → the web UI's PR (private repo) · F4 lands in both

Standards conformity

The round 1 audit stands. There are no new change-level violations.
Set — Clean: §14 (one capability, three clients, SDK and CLI merged first). Rule 6 name identity isn't load-bearing here, because no CI seam resolves the web UI against the SDK or CLI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants