# Defang

Finds the URLs, email addresses and IP addresses in a text and defangs them, so nobody clicks or resolves an indicator by accident.

```tsx
import * as React from "react"
import { Field, FieldLabel } from "@zenoxai/ui/components/field"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@zenoxai/ui/components/table"
import { Textarea } from "@zenoxai/ui/components/textarea"
import { ToggleGroup, ToggleGroupItem } from "@zenoxai/ui/components/toggle-group"
import { defang, findIndicators, refang } from "@zenoxai/ui/lib/defang"

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

```tsx
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](/patterns/copy-button). See
[Data display](/guidelines/data-display#indicators).

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.

```tsx
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.

```tsx
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](/patterns/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

`import { ... } from "@zenoxai/ui/lib/defang"`

### 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)`.

| Parameter | Type | Description |
| --- | --- | --- |
| text | `string` | The text to scan. |
| types (optional) | `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 | Description |
| --- | --- | --- |
| text | `string` | The text to scan. |
| types (optional) | `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): 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.

| Parameter | Type | Description |
| --- | --- | --- |
| value | `string` | 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.

| Parameter | Type | Description |
| --- | --- | --- |
| text | `string` | 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.

| Field | Type | Description |
| --- | --- | --- |
| type | `IndicatorType` | What the indicator is. A host with no scheme and no path, such as `www.acme.example` or `evil[.]example`, is a `domain`. |
| text | `string` | The indicator as written: `input.slice(start, end)`. |
| value | `string` | The live value, with every defang marker reversed. |
| start | `number` | Where the match starts, in UTF-16 code units, like `String.prototype.slice`. |
| end | `number` | Where the match ends, exclusive. |
| state | `IndicatorState` | Whether the indicator can be followed as written. |

