Defang
betaFinds the URLs, email addresses and IP addresses in a text and defangs them, so nobody clicks or resolves an indicator by accident.
@zenoxai/ui/lib/defang Markdown| Type | As written | State |
|---|---|---|
| ipv4 | 203.0.113.25 | live |
| url | https://cdn-update.example.net/a/b.js | live |
| billing@acme-invoices.example | live |
function DefangDemo() {
const [text, setText] = React.useState(
"Beacon from 203.0.113.25 to https://cdn-update.example.net/a/b.js, phish sent by billing@acme-invoices.example, kit v1.2.3."
)
const [mode, setMode] = React.useState("defang")
const result = mode === "defang" ? defang(text) : refang(text)
const matches = findIndicators(text)
return (
<div className="flex w-full flex-col gap-4">
<div className="grid gap-4 md:grid-cols-2">
<Field>
<FieldLabel htmlFor="defang-text">Text</FieldLabel>
<Textarea
id="defang-text"
value={text}
onChange={(event) => setText(event.target.value)}
rows={4}
/>
</Field>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium" id="defang-result">
Result
</span>
<ToggleGroup
variant="segmented"
size="sm"
value={[mode]}
onValueChange={(value) => {
if (value[0]) setMode(value[0])
}}
aria-label="Function"
>
<ToggleGroupItem value="defang">defang</ToggleGroupItem>
<ToggleGroupItem value="refang">refang</ToggleGroupItem>
</ToggleGroup>
</div>
<output
aria-labelledby="defang-result"
className="min-h-24 rounded-md bg-surface-muted px-3 py-2 font-mono text-sm wrap-anywhere text-text-secondary"
>
{result}
</output>
</div>
</div>
<div className="overflow-hidden rounded-lg border border-border">
<Table aria-label="Indicators found">
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>As written</TableHead>
<TableHead>State</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{matches.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="text-text-muted">
No indicators in the text.
</TableCell>
</TableRow>
) : (
matches.map((match) => (
<TableRow key={match.start}>
<TableCell className="font-mono">{match.type}</TableCell>
<TableCell className="font-mono wrap-anywhere">
{match.text}
</TableCell>
<TableCell>{match.state}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
)
}Usage
import { defang, defangIndicator } from "@zenoxai/ui/lib/defang"
// A notification body, a report paragraph, a Slack message
const body = defang(alert.description)
// A value the app knows is an indicator, going into prose
const line = `Blocked ${defangIndicator(alert.domain)}`
Defang the indicators in text that people read outside the platform: report PDFs, emails, notification bodies, Slack and Teams messages. CSV and JSON exports stay live: they carry data, not presentation.
Inside the platform, an indicator in its own column or field stays live, in font-mono, with
Copy through Value Actions. See
Data display.
These are plain functions, with no React and no DOM: they run in a Server Component, a worker and the browser alike.
Examples
A value the app already knows
defang detects a bare domain only when it is already defanged (evil[.]com): a live one such
as acme.com would also match report.pdf and v1.2. When a structured field holds a domain,
pass it to defangIndicator, which defangs whatever it gets and never throws.
defangIndicator("acme-invoices.example") // "acme-invoices[.]example"
defangIndicator("hxxps://evil.com/a") // "hxxps://evil[.]com/a"
Back to live values for a tool
refang reads the variants analysts type (hxxp, [.], (.), {.}, [dot], (dot), \.,
[@], (@), [at], (at), [:], [://]) and rewrites only the indicators it finds, so a[:]
in a code sample stays as written.
refang("soc[at]acme(.)com, hxxps://evil[.]com/a[.]php")
// "soc@acme.com, https://evil.com/a.php"
Linking the live URLs only
findIndicators returns every match with its type, its text as written, its live value, its
position and whether it is live, defanged or partial.
Linkified Text uses it and links only the live http and https URLs.
Rules
One convention, the one CyberChef and ioc-fanger write, except that :// stays as it is: no
autolinker recognizes hxxps://.
| Kind | Live | Defanged |
|---|---|---|
| URL | https://login-verify-acme.com/index.php |
hxxps://login-verify-acme[.]com/index[.]php |
| URL | https://www.paypal.com@evil.example/ |
hxxps://www[.]paypal[.]com[@]evil[.]example/ |
soc@acme.com |
soc[@]acme[.]com |
|
| IPv4 | 203.0.113.25:8080 |
203[.]0[.]113[.]25:8080 |
| IPv6 | 2001:db8::1 |
2001[:]db8[:][:]1 |
| Domain | www.acme-invoices.example |
www[.]acme-invoices[.]example |
- Every dot of a URL is bracketed, in the path and the query too, so an embedded
?q=https://evil.example/xdoes not stay live. Everyhttpbecomeshxxpwith its case kept, and an@in the authority becomes[@]. - An indicator written in another variant, or only partly defanged (
hxxps://evil.com), comes out in the same convention, never with a live host left. defang(defang(text))isdefang(text), andrefang(defang(value))isvaluefor a live indicator that contains no defang marker of its own.- A version number such as
1.2.3.4is detected as an IPv4 address: a missed address costs more than a few brackets. Runs with more dots (1.3.6.1.4.1), times, MAC addresses and::are not. - The scanner runs in linear time, so attacker-written text cannot make it hang. A URL has no length cap: all of it is defanged, however long.
API reference
Generated from the TypeScript source and its JSDoc.
defang
defang(text: string, types?: readonly IndicatorType[]): stringdefang rewrites every indicator found in a text in one convention and leaves the rest of the text as it is: hxxps://acme[.]example/index[.]php, soc[@]acme[.]example, 203[.]0[.]113[.]25, 2001[:]db8[:][:]1.
Every dot of a URL is bracketed, in the path and the query too, every http becomes hxxp with its case kept, and an @ in the authority becomes [@]. An indicator written in another variant, or only partly defanged, comes out in the same convention. defang(defang(text)) equals defang(text).
| Parameter | Type |
|---|---|
text | stringThe text to scan. |
typesoptional | readonly IndicatorType[]Limits the kinds rewritten. All of them by default. |
- Returns
string - The text with its indicators defanged.
findIndicators
findIndicators(text: string, types?: readonly IndicatorType[]): IndicatorMatch[]findIndicators returns the URLs, email addresses, IPv4 and IPv6 addresses and defanged domains in a text, live or defanged, in order and never overlapping: a URL wins over the email, address or domain inside it.
A live bare domain such as acme.example is not detected: without a list of top-level domains, report.pdf and v1.2 would match. Pass a value known to be a domain to defangIndicator instead.
| Parameter | Type |
|---|---|
text | stringThe text to scan. |
typesoptional | readonly IndicatorType[]Limits the result to some kinds. All of them by default. |
- Returns
IndicatorMatch[] - The indicators found, with their positions and live values.
defangIndicator
defangIndicator(value: string): stringdefangIndicator defangs one value the app already knows is an indicator, such as a domain from a structured field, which defang would not detect in prose. It infers the type from the value, never throws and validates nothing.
| Parameter | Type |
|---|---|
value | stringOne indicator, live or defanged in any variant. |
- Returns
string - The value in defang's convention.
refang
refang(text: string): stringrefang turns the defanged and partly defanged indicators found in a text back into live values, for pasting into a tool that needs them. It reads hxxp, [.], (.), {.}, [dot], (dot), \., [@], (@), [at], (at), [:] and [://], and rewrites only the indicators it finds, so a[:] in a code sample stays as written.
| Parameter | Type |
|---|---|
text | stringThe text to scan. |
- Returns
string - The text with its defanged indicators live again.
IndicatorType
"url" | "email" | "ipv4" | "ipv6" | "domain"IndicatorType names what an indicator is.
IndicatorState
"live" | "defanged" | "partial"IndicatorState says whether an indicator can be followed as written: live has no defang marker, defanged cannot be followed or resolved, partial mixes both (hxxps://evil.example, https://evil[.]example).
IndicatorMatch
IndicatorMatch is one indicator that findIndicators found in a text.
| Field | Type |
|---|---|
type | IndicatorTypeWhat the indicator is. A host with no scheme and no path, such as
|
text | stringThe indicator as written: |
value | stringThe live value, with every defang marker reversed. |
start | numberWhere the match starts, in UTF-16 code units, like |
end | numberWhere the match ends, exclusive. |
state | IndicatorStateWhether the indicator can be followed as written. |