Metadata-Version: 2.4
Name: oamx
Version: 0.1.0
Summary: Read the OWASP Amass asset database and pipe it into everything else
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/coolhandle01/oamx
Project-URL: Issues, https://github.com/coolhandle01/oamx/issues
Keywords: amass,osint,recon,attack-surface,bug-bounty,open-asset-model
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: pylint>=3.2; extra == "dev"
Requires-Dist: ruff==0.16.0; extra == "dev"
Dynamic: license-file

# oamx

**Read the OWASP Amass asset database and pipe it into everything else.**

Zero dependencies. Read-only. Works across Amass v4 and v5.

---

## The problem

This is the recon one-liner. Some version of it appears in every Amass tutorial,
blog post and cheat sheet written in the last five years:

```bash
amass enum -passive -d example.com -o subs.txt
httpx -l subs.txt -silent | nuclei -severity high,critical
```

Since Amass v5 (August 2025), it writes an **empty** `subs.txt`.

v5 moved results into an asset database and stopped populating the text output.
The scan works. The data is there. But `-o` produces nothing, httpx probes
nothing, nuclei finds nothing, and the pipeline **exits 0**. It doesn't fail —
it succeeds at doing nothing, quietly, on a schedule, possibly for months.

Amass is a producer with no consumer story. The documented way out is
`amass subs -names -d example.com`, which gives you a flat list of hostnames and
nothing else — no IPs, no netblocks, no ports, no provenance, no scoping, no
"what changed since yesterday". Everyone else is pasting `sqlite3 json_extract`
incantations out of GitHub issues.

`oamx` is the consumer.

```bash
oamx names -d example.com --resolved-only | httpx -silent | nuclei -severity high,critical
```

---

## Install

```bash
pipx install oamx          # or: pip install oamx
```

Python 3.10+. No runtime dependencies — deliberately. A recon pipeline that
breaks because of a transitive dependency resolution is worse than no tool.

There are two entry points and they are interchangeable:

```bash
oamx names -d example.com          # the console script
python -m oamx names -d example.com   # when PATH is not set up for you
```

The second is the one that keeps working inside a container, a
`pip install --target` layout, or anywhere the scripts directory is not on
`PATH`. Both are checked against the built wheel on every release.

---

## Quickstart

```bash
# What is actually in my database?
oamx doctor

# Names, scoped to one target, that actually resolve
oamx names -d example.com --resolved-only

# Everything Amass found listening, ready for httpx or nuclei
oamx targets -d example.com --urls

# What genuinely appeared in the last day
oamx names -d example.com --new --since 24h

# The whole picture, with provenance, for storage or an agent to reason over
oamx json -d example.com > assets.jsonl
```

`oamx doctor` is the one to run first when a scan "found nothing":

```
database        /home/you/.config/amass/amass.sqlite
layout          v5 (entities/edges)
provenance      yes
assets          22

  FQDN                 8
  Service              3
  IPAddress            3
  Identifier           2
  URL                  1
  TLSCertificate       1
  SomeFutureAssetType  1
  Netblock             1
  ContactRecord        1
  AutonomousSystem     1
```

That is real output from the test fixture rather than an illustration, and
everything below the `database` line is asserted against a live run — see
`tests/test_readme.py`. `SomeFutureAssetType` is in the fixture deliberately:
an asset type this release has never heard of still gets counted, and still
comes out of `oamx json` with a usable value.

---

## Commands

| Command | Output |
| --- | --- |
| `names` | fully qualified domain names |
| `ips` | IP addresses (`--ipv4` / `--ipv6`) |
| `cidrs` | netblocks in CIDR notation |
| `asns` | autonomous system numbers |
| `urls` | discovered URLs |
| `certs` | TLS certificates |
| `services` | responding network services |
| `orgs` | organisations |
| `emails` | email addresses |
| `targets` | `host:port` pairs, or `--urls` for `scheme://host:port` |
| `dns` | `name<TAB>RRTYPE<TAB>target` triples |
| `json` | every matching asset as JSONL, with provenance |
| `graph` | every matching relation as JSONL |
| `stats` | counts by asset type |
| `doctor` | what the database is and what is in it |

## Filters

Every command takes the same set:

| Flag | Effect |
| --- | --- |
| `--db PATH` | database path (default: auto-discover the usual per-platform locations) |
| `-d, --domain` | scope to a root domain; repeatable and comma-separated |
| `--scope-depth N` | graph hops used to scope non-name assets (default 2, `0` disables) |
| `--since DUR` | last seen within `24h`, `7d`, `2w` … |
| `--new` | with `--since`, match *first* seen — genuinely new, not re-confirmed |
| `--resolved-only` | only names with a DNS record |
| `--source NAME` | only assets asserted by this Amass plugin |
| `--exclude-source NAME` | drop assets whose *only* sources are these |
| `--min-confidence N` | drop assets below this source confidence (0–100) |
| `--json` | JSONL with provenance instead of bare values |
| `--count` | just the number |
| `--fail-empty` | exit 1 if nothing matched, for CI |

---

## Why it works the way it does

Four decisions worth knowing about, because they change what you get back.

**It introspects the schema instead of pinning to a version.**
Amass has renamed its storage at least three times — v3 graph files, v4
`assets`/`relations`, v5 `entities`/`edges`, plus column churn inside v5 point
releases. `oamx` reads whatever tables and columns it finds and maps them onto
one logical model. Asset types that didn't exist when this was written still
come out with a usable value rather than raising. The adapter absorbs the churn
so your pipeline doesn't.

**Scoping follows the graph, but names are matched by suffix.**
`-d example.com` seeds on hostname suffix, then walks out `--scope-depth` hops
to pick up the IPs, netblocks and certificates attached to those names. But a
*hostname* reached by graph proximity is never pulled into scope. Without that
rule, one CNAME to a shared CDN drags the CDN's other customers into your
target list. Use `--scope-depth 1` to stay on directly-resolved addresses, or
`0` for names only.

**Rows that reduce to the same thing are merged.**
`API.Example.COM.` from a certificate SAN and `api.example.com` from a DNS
answer are two database rows and one host. `oamx` merges them: provenance is
unioned, the higher confidence wins, and the sighting window widens to cover
every contributing row. Merging happens *before* time and source filters, so a
host is never judged on only one of its sightings.

**Rediscovery is not discovery.**
`--new --since 24h` reports hosts first seen in that window. A host you've known
for a month, which a second data source just re-confirmed, is not new and won't
alert. That false-positive class is precisely what trains people to ignore their
monitoring.

---

## Output schema

`--json` emits one object per line, versioned so downstream code can depend on
it even as Amass changes underneath:

```json
{
  "schema": "oamx/1",
  "kind": "asset",
  "id": 3,
  "type": "FQDN",
  "value": "api.example.com",
  "first_seen": "2026-06-24 12:00:00.000000000+00:00",
  "last_seen": "2026-07-24 10:00:00.000000000+00:00",
  "sources": [{"name": "crtsh", "confidence": 95}, {"name": "DNS-IP", "confidence": 100}],
  "attrs": {"name": "api.example.com"}
}
```

`graph` emits `"kind": "edge"` records with `from`, `to`, `label`, and decoded
DNS record types (`rr_type: 1` becomes `rr_name: "A"`, because nobody wants to
grep for `5`).

---

## As a library

```python
from oamx.integrations import query, values

hosts = values("names", domains=["example.com"], resolved_only=True)
records = query("all", domains=["example.com"], since="7d")
```

`query` returns the same records `oamx json` emits, as dicts. `values` returns
just the values. Both raise `OamxError` with a message written to be read.

Reading is all it does: the database is opened `mode=ro`, so nothing here can
start a scan or send traffic. That is a useful property if you are handing it
to an agent — but oamx ships no agent-framework adapter, deliberately. Build
that on your side, where your own conventions for tool schemas and error
handling live.

The package ships `py.typed`, so annotations survive into whatever imports it.

---

## Known limits

Stated plainly, because a recon tool that overstates itself is worse than useless.

- **SQLite only.** Amass also supports PostgreSQL and Neo4j. The reader is
  written behind an interface so those are clean additions, but shipping
  untested database drivers would defeat the point.
- **Field names are verified for `FQDN`, `IPAddress`, `Service` and
  `Identifier`** against the Open Asset Model documentation and, for
  `Identifier`, the struct in `owasp-amass/open-asset-model` itself. The
  remaining ~17 asset types use a best-effort key list with a generic
  fallback. If one of them extracts the wrong field for you, that's a one-line
  fix in `model.py` and a very welcome issue — `Identifier` was exactly that,
  reporting the `unique_id` dedupe key where the value lives in `id`.
- **The whole graph is loaded into memory** to make scoping and merging correct.
  Fine for target-scoped databases; if yours has millions of entities, this will
  want a streaming path.
- **Amass v3 graph files are not supported.** Only the SQLite asset database.

## Compatibility

| Amass | Layout | Status |
| --- | --- | --- |
| v5.x | `entities` / `edges` | tested |
| v4.x | `assets` / `relations` | tested |
| v3.x | graph files | not supported |

Both layouts are covered by fixtures built from the documented schemas, not from
guesses.

---

## Development

```bash
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```

122 tests in about a second, gated on branch coverage rather than line
coverage — this codebase is mostly branching, and line coverage calls a
half-tested `if` fully covered. `ruff`, `mypy --strict` and `pylint` run from
the same `dev` extra, so what CI runs and what you run cannot drift; see
`CONTRIBUTING.md` for the order.

The dev extra is not part of the zero-dependency promise, which is about what
an *installed* oamx pulls in. CI proves that separately by installing the
package alone and asserting nothing third-party came with it.

If you add behaviour, break it on purpose first and check something goes red.
A test that has never been seen to fail has not been shown to test anything.

## Upstream

The gap this fills is arguably an Amass issue, not a third-party one. If the
project wants an `amass export` that emits structured, scoped, provenance-carrying
output, the schema in `oamx/model.py` is a reasonable starting point and this
tool should stop being necessary. That would be the good outcome.

Licensed Apache-2.0, matching the Amass ecosystem.
