Skip to content
UI

Defang

beta

Finds 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
Result
Beacon from 203[.]0[.]113[.]25 to hxxps://cdn-update[.]example[.]net/a/b[.]js, phish sent by billing[@]acme-invoices[.]example, kit v1.2.3.
TypeAs writtenState
ipv4203.0.113.25live
urlhttps://cdn-update.example.net/a/b.jslive
emailbilling@acme-invoices.examplelive
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/
Email 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/x does not stay live. Every http becomes hxxp with 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)) is defang(text), and refang(defang(value)) is value for a live indicator that contains no defang marker of its own.
  • A version number such as 1.2.3.4 is 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[]): string

defang 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).

ParameterType
textstring

The text to scan.

typesoptionalreadonly 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.

ParameterType
textstring

The text to scan.

typesoptionalreadonly 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): string

defangIndicator 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.

ParameterType
valuestring

One indicator, live or defanged in any variant.

Returns string
The value in defang's convention.

refang

refang(text: string): string

refang 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.

ParameterType
textstring

The 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.

FieldType
typeIndicatorType

What the indicator is. A host with no scheme and no path, such as www.acme.example or evil[.]example, is a domain.

textstring

The indicator as written: input.slice(start, end).

valuestring

The live value, with every defang marker reversed.

startnumber

Where the match starts, in UTF-16 code units, like String.prototype.slice.

endnumber

Where the match ends, exclusive.

stateIndicatorState

Whether the indicator can be followed as written.