Useful? A star is how other developers find it — ★ GitHub · letools.dev/tools/ips-le
Somebody has to check the firewall allow-list against the change request, the connection string against the network diagram, the fetch path against the SSRF review. Usually without a checkout, always without the editor open.
grep -rE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' half-serves them. It finds
no IPv6 at all. It calls 1.2.3 an address. It reports
2001:0db8::0001 and 2001:db8::1 as two different things — they are
one address. And it calls 010.1.1.1 an address without saying which
address, which is the whole reason that string is an SSRF bypass.
Install
Sixty seconds
|
# the point of the whole thing:
| |
stderr, in full, for one of the fixture documents that ships with the
crate — ips-le fixtures/documents/network.yaml:
fixtures/documents/network.yaml:3:11 0.0.0.0 0.0.0.0 reserved
fixtures/documents/network.yaml:4:15 10.20.30.40 10.20.30.40 private
fixtures/documents/network.yaml:6:11 127.0.0.1 127.0.0.1 loopback
fixtures/documents/network.yaml:7:11 2001:0db8::0001 2001:db8::1 documentation
fixtures/documents/network.yaml:9:5 10.0.0.0/8 10.0.0.0/8 private
fixtures/documents/network.yaml:10:5 192.168.0.0/16 192.168.0.0/16 private
fixtures/documents/network.yaml:11:11 169.254.169.254 169.254.169.254 link-local
fixtures/documents/network.yaml:12:11 aa:bb:cc:dd:ee:ff aa:bb:cc:dd:ee:ff global
fixtures/documents/network.yaml:14:11 10.0.0.7 10.0.0.7 private
fixtures/documents/network.yaml:14:26 10.0.0.8 10.0.0.8 private
10 addresses in 1 file
The last two are one line — fallback: 10.0.0.7 # was 10.0.0.8. Both
addresses are reported, because an address in a comment is one a
reviewer came for; only the first carries the key fallback, because
the file does not bind to the other one.
What it answers
One address, one form. IPv6 is normalized per
RFC 5952 —
2001:0db8:0000:0000:0000:0000:0000:0001, 2001:db8:0:0:0:0:0:1,
2001:0db8::0001 and 2001:DB8::1 all come back as 2001:db8::1.
Sorting the raw text gives four addresses; sorting the normalized form
gives one.
What it is for. Every address carries a class: loopback,
private, link-local, cgnat, multicast, broadcast, reserved,
documentation, unique-local, global. An IPv4-mapped IPv6 address
takes the IPv4 class, so ::ffff:127.0.0.1 is loopback rather than
global — which is the miss an allow-list review is looking for.
Where it is. Line, column, and the key it sits under: JSON, YAML,
TOML, INI, dotenv, CSV and logs all supply one. Everything else is still
scanned — the search runs over the bytes, so a .tf, a .rules or a
rotated log yields its addresses and only loses the key path.
Blocks, with their arithmetic. A CIDR finding carries prefix,
network, broadcast, last and hosts — hosts as a decimal
string, because ::/0 holds 2^128 addresses.
What it refuses
Where the text supports more than one reading, ips-le reports the
text, names the ambiguity, and stops.
$ ips-le --stdin <<< '010.1.1.1'
<stdin>:1:1 010.1.1.1 refused OctalHazard
0 addresses in 1 file
1 refused
Six reasons: octal_hazard, ambiguous_version, integer_form,
malformed_address, prefix_out_of_range, mac_ambiguous. Each one is
a place where two answers are equally defensible, and
SPEC.md says exactly when each fires.
The two that matter most:
010.1.1.1is not resolved. A leading-zero octet is octal to some resolvers and decimal to others, so that text names two different hosts. Neither reading appears anywhere in the output — a tool that picked one would be the thing hiding the bug.2130706433is decoded only next to the flag. Under an address key it is reported asinteger_form, with127.0.0.1inside the refusal message. What you never get is a loopback address quietly appearing in a list of addresses with the flag gone.
A refusal is a finding, not a failure. It does not move the exit
code, and no filter can hide it — --class private still shows you the
octal hazard, because that is the finding a filtered report would most
regret dropping. --strict is there for the pipeline that wants an
unresolved ambiguity to stop the build.
It never touches a network
No DNS, no geolocation, no ASN, no WHOIS, no reachability check, no telemetry. Not behind a flag, not once. Classification is arithmetic over the bits and the IANA registries; a lookup would make the answer depend on the network the auditor happened to be sitting on.
It also never rewrites a file, and it never gives a verdict. It says what an address is, never whether it should be there.
Exit codes
Following grep: 0 addresses found · 1 none found · 2
malformed question. Finding none is an answer, and so is a refusal — a
file of nothing but ambiguities exits 1. --strict turns a refusal, or
a file that could not be read, into a 2.
if ; then
fi
Options
Taken from ips-le --help, which is the authority.
| Option | What it does |
|---|---|
--format <format> |
Force a format instead of inferring it from the file name; an unknown name still scans, it just reports no key paths |
--kind <kind> |
Report only ipv4, ipv6, cidr or mac; repeatable |
--class <class> |
Report only one class, e.g. private or global; repeatable |
--strict |
Exit 2 if anything was refused or any file could not be read, rather than reporting it and carrying on |
--stdin |
Read one document from stdin |
--hidden |
Walk hidden files and directories too |
--no-ignore |
Walk files that .gitignore excludes |
A filter narrows what this tool claims, never what it declined to claim:
a refusal survives --kind and --class, because the finding a filtered
report would hide is the one most worth seeing.
As an MCP server
Two tools, one envelope ({ ok, data, diagnostics, meta }):
extract_ips— takes document text, returns findings. Touches no filesystem.ips_le_scan— takes a path, reads the tree.
ok means the scan ran, never that the answer was yes. A model reading
2001:0db8::0001 and 2001:db8::1 out of a diff will usually call them
two addresses; this is how it stops having to guess.
Documentation
- SPEC.md — the refusal table, the classification table, the output schema, the non-goals.
- CHANGELOG.md — what changed and when.
- AGENTS.md — how the code is written and reviewed. Linked to the repository rather than relatively: it is deliberately excluded from the published package, so a relative link would be broken for anyone reading this on crates.io or from an unpacked tarball.
More from the LE family
Sixteen single-purpose tools for the work in front of every model. Each ships a Rust CLI and an MCP server. One page: letools.dev
Get it out
- String-LE — Extract every string in a codebase, with its position, so a person can read them
- Numbers-LE — Extract every hardcoded number in a codebase, so a person can check them
- Units-LE — Extract every quantity with its unit, normalized, and refuse the ambiguous ones by name
- Dates-LE — Extract every date and timestamp, and the exact instant each one resolves to
- IDs-LE — Extract every UUID, ULID, NanoID, ObjectId and Snowflake, and decode the time inside
- IPs-LE — Extract every IP address, CIDR block and MAC, normalized and classified by scope
- URLs-LE — Extract every URL in a codebase, with its protocol and exact position
- Paths-LE — Extract every file path in a codebase, and say whether it still points at anything
- Colors-LE — Extract every color in a codebase, and say which ones are not in your palette
Check it
- Regex-LE — Find every regex in a codebase, and report which can be driven into catastrophic backtracking
- Versions-LE — Find where one dependency is constrained differently across a repository's manifests
- i18n-LE — Identify the i18n library a project uses, then audit its catalogs by that library's rules
- Scrape-LE — Check whether a page is scrapeable before the scraper is written, and say when it cannot tell
Guard it
- Secrets-LE — Find hardcoded credentials in a codebase, and never print one into the report
- EnvSync-LE — Compare the dotenv files in a tree, and say which keys are missing from which
- Unicode-LE — Find the Unicode that hides meaning — bidi controls, invisibles, homoglyphs, mixed scripts
Each stands on its own: no shared crate, no published core. Where two of them agree, it is because the same answer was right twice.
Contact — nolindnaidoo.com · GitHub · LinkedIn
Also by nolindnaidoo
Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. Their own tools, their own voice — not part of the LE family.
- pixelcoords — Freeze your screen, mark regions, get pixel-exact coordinates and crops pixelcoords.dev · crates.io · docs.rs
- pixelactions — Consume human-verified coordinates, perform the interaction, confirm it landed pixelactions.dev · crates.io · docs.rs
License
MIT — see LICENSE.