83 Commits

Author SHA1 Message Date
kpcyrd
37b1d0e067 Release v0.11.2 2019-05-13 06:33:11 +02:00
kpcyrd
056499fb64 Merge pull request #92 from kpcyrd/current_exe
Fix current_exe path issue on openbsd
2019-05-13 06:11:24 +02:00
kpcyrd
231eba3a37 Update nude-rs dependency 2019-05-13 05:23:13 +02:00
kpcyrd
c205df63a8 Fix current_exe path issue on openbsd 2019-05-13 04:58:26 +02:00
kpcyrd
9a12ea8e6a Update dependencies and remove workspace workaround 2019-05-13 04:51:18 +02:00
kpcyrd
c81fdde5f9 Add new dns modules 2019-04-30 15:32:48 +02:00
kpcyrd
0dcf5f4d28 Update openbsd install instructions 2019-04-26 09:13:08 +02:00
kpcyrd
2dfef8d9a3 Release v0.11.1 2019-04-25 17:23:52 +02:00
kpcyrd
3810b7c87e Merge pull request #91 from kpcyrd/openbsd
Fix build for openbsd
2019-04-25 17:11:18 +02:00
kpcyrd
da85aa2eb3 Unveil /dev/urandom 2019-04-25 08:14:00 +02:00
kpcyrd
ea70815589 Fix build for openbsd 2019-04-25 07:37:09 +02:00
kpcyrd
3c2386ff48 Revert rocket workaround again
https://github.com/SergioBenitez/Rocket/issues/905
2019-04-24 13:47:39 +02:00
kpcyrd
1068fccf0f Bump module versions 2019-04-24 13:42:54 +02:00
kpcyrd
eb885b06ab Release v0.11.0 2019-04-22 03:10:17 +02:00
kpcyrd
8e2b430396 Update readme text 2019-04-22 02:45:30 +02:00
kpcyrd
d3bd38ce6e Bump dependencies 2019-04-22 02:28:40 +02:00
kpcyrd
e9f7cd667f Document into_blob 2019-04-22 02:10:56 +02:00
kpcyrd
cb86f21a95 Merge remote-tracking branch 'origin/docs' 2019-04-22 00:14:27 +02:00
kpcyrd
eb7f38b9ed Update docs 2019-04-17 19:19:32 +02:00
kpcyrd
7c50200e7e Merge pull request #86 from kpcyrd/imgs
Add image forensics support
2019-04-16 13:48:41 +02:00
kpcyrd
d77800b6fd Docker: Switch back to debian 2019-04-16 12:05:02 +02:00
kpcyrd
e3be152a98 Add --paths flag to select command 2019-04-15 17:12:02 +02:00
kpcyrd
c8ccfa0cfc Change padding direction 2019-04-15 16:08:20 +02:00
kpcyrd
724bcdc344 Use nude-rs from crates.io 2019-04-15 15:39:46 +02:00
kpcyrd
0e9bcaf82e Pad blob ids to uniform length 2019-04-15 01:15:42 +02:00
kpcyrd
1e6ee04a36 Use base58 instead of hex for blobs 2019-04-14 18:54:31 +02:00
kpcyrd
5df39f758e Add img_load and img_nudity 2019-04-14 17:39:28 +02:00
kpcyrd
72bdc83fd3 Update dependencies 2019-04-14 15:46:10 +02:00
kpcyrd
657dc35fda Bump dependencies 2019-04-03 17:10:05 +02:00
kpcyrd
90ea945f79 Add gpg importer to github module
Shout out to @anthraxx for the hint
2019-04-03 17:09:29 +02:00
kpcyrd
0f7ad254ec Add endpoint for badges 2019-03-25 16:16:32 +01:00
kpcyrd
96e539fdbc Fix update terminal output 2019-03-25 10:43:30 +01:00
kpcyrd
2ef48dd830 Add more badges 2019-03-25 10:32:24 +01:00
kpcyrd
a5c92a5e3a Clear outdated counter after successful update 2019-03-25 08:03:51 +01:00
kpcyrd
f4f785f888 Add a name field to emails 2019-03-25 07:37:00 +01:00
kpcyrd
1904133294 Refactor script for-loops 2019-03-24 20:41:09 +01:00
kpcyrd
e3f4d1f837 Add image downloader to instagram module 2019-03-24 11:21:27 +01:00
kpcyrd
703d1814d0 Add cryptographic hash functions 2019-03-18 07:31:28 +01:00
kpcyrd
dd26c49739 Add fsck subcommand 2019-03-18 03:09:20 +01:00
kpcyrd
62d1b9aa06 Add image decoder 2019-03-18 01:50:27 +01:00
kpcyrd
c033f08e64 Support importing images from disk 2019-03-17 06:50:31 +01:00
kpcyrd
d11e7bb009 Restructure argument handling 2019-03-17 05:45:24 +01:00
kpcyrd
c42783c338 Refactor struct enums 2019-03-17 01:51:01 +01:00
kpcyrd
3a787f647b Add exif parser 2019-03-17 01:30:27 +01:00
kpcyrd
c88801af82 Add home assistant script 2019-03-12 16:58:47 +01:00
kpcyrd
7abde1374d Send blobs to child process 2019-03-12 16:05:52 +01:00
kpcyrd
e715a7d7c1 Add blob storage 2019-03-12 15:42:47 +01:00
kpcyrd
812a2f4d27 Add image model to database 2019-03-08 19:02:36 +01:00
kpcyrd
8025418e2f Document config file locations 2019-03-08 06:11:49 +01:00
kpcyrd
edff6eda43 Add keybase modules 2019-03-05 14:16:11 +01:00
kpcyrd
bdd46eb9be Fix docker-registry build 2019-03-04 18:41:58 +01:00
kpcyrd
c4d0cfd0d7 Improve sn0int run interface 2019-03-04 07:17:08 +01:00
kpcyrd
30f848bcf7 Merge pull request #81 from kpcyrd/registry
Registry and update improvements
2019-03-03 05:17:00 +01:00
kpcyrd
66c2007a16 Allow setting private modules in the config file 2019-03-03 04:13:54 +01:00
kpcyrd
ee691942f4 Try detecting private modules and skip update 2019-03-03 00:41:38 +01:00
kpcyrd
7bc4dc4c6a Automatically check for new modules 2019-03-02 16:23:58 +01:00
kpcyrd
e9a4323f52 Add endpoint to query latest publish 2019-03-02 16:16:49 +01:00
kpcyrd
f54b4d8c99 Add health endpoint to registry 2019-03-01 04:55:00 +01:00
kpcyrd
8951147b9a Release v0.10.0 2019-02-28 17:20:24 +01:00
kpcyrd
2886596893 Add tests for json functions 2019-02-28 16:29:00 +01:00
kpcyrd
e896e11d7c Add ddwrt script 2019-02-28 16:29:00 +01:00
kpcyrd
cbb6a87ca2 Merge pull request #80 from kpcyrd/ring
Bump ring for aarch64 support
2019-02-28 16:26:10 +01:00
kpcyrd
09e1514391 Bump ring 2019-02-27 18:10:46 +01:00
kpcyrd
8a6f8aaca0 Merge pull request #79 from kpcyrd/docs
Document structs
2019-02-27 16:36:01 +01:00
kpcyrd
a22caa4ef4 Skip lto to avoid compiler bug 2019-02-27 14:40:19 +01:00
kpcyrd
e3a84dfe89 Document structs 2019-02-24 20:50:03 +01:00
kpcyrd
b938d9c7f5 Refactor structs 2019-02-24 17:08:18 +01:00
kpcyrd
454a769f84 Merge pull request #78 from kpcyrd/breaches
Add breaches
2019-02-23 22:03:29 +01:00
kpcyrd
8150ad9483 Support multiple passwords per breach
Also update password-less links to a breach if we insert a 2nd link that
contains a password between the same breach and email.
2019-02-21 16:22:09 +01:00
kpcyrd
b9fddedbb5 Fix <= and >= in db queries 2019-02-19 16:58:54 +01:00
kpcyrd
b48c8728fd Add breaches to database 2019-02-19 16:43:36 +01:00
kpcyrd
af9087b80b Merge pull request #75 from kpcyrd/accounts
Add accounts
2019-02-19 16:30:10 +01:00
kpcyrd
b8b535c19a Disable docker test because of max build time 2019-02-15 18:39:10 +01:00
kpcyrd
344d28ec56 Add function to test sn0int semver 2019-02-15 14:29:07 +01:00
kpcyrd
30d3c1ac56 Set useragent for api client correctly 2019-02-15 14:24:14 +01:00
kpcyrd
0748297a42 Improve namechk error handling 2019-02-15 14:20:06 +01:00
kpcyrd
e9d9e9925a Add first attempt on namechk script 2019-02-14 17:14:36 +01:00
kpcyrd
c0c2c31b65 Add displayname field 2019-02-14 16:07:39 +01:00
kpcyrd
17f4682476 Add accounts to database 2019-02-12 16:38:07 +01:00
kpcyrd
4ce8f00ad8 Add additional context to download failures 2019-02-12 15:13:06 +01:00
kpcyrd
e5a9f4cfba Document optional dependencies on debian 2019-02-12 15:01:12 +01:00
kpcyrd
fcc6509a69 Make unknown script metadata non-fatal 2019-02-10 07:00:23 +01:00
kpcyrd
14339dea2f Fix mremap seccomp filter 2019-02-09 19:00:44 +01:00
126 changed files with 6912 additions and 2029 deletions

2161
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "sn0int"
version = "0.9.1"
version = "0.11.2"
description = "Semi-automatic OSINT framework and package manager"
authors = ["kpcyrd <git@rxv.cc>"]
license = "GPL-3.0"
@@ -9,6 +9,13 @@ categories = ["command-line-utilities"]
readme = "README.md"
edition = "2018"
[profile.release]
# skip lto to avoid compiler bug:
# https://github.com/kpcyrd/sn0int/issues/77
# https://github.com/rust-lang/rust/issues/58674
opt-level = 1
lto = false
[badges]
travis-ci = { repository = "kpcyrd/sn0int" }
@@ -17,8 +24,8 @@ members = ["sn0int-registry/sn0int-common",
"sn0int-registry"]
[dependencies]
sn0int-common = { version="0.4.0", path="sn0int-registry/sn0int-common" }
rustyline = "3"
sn0int-common = { version="0.6.0", path="sn0int-registry/sn0int-common" }
rustyline = "4.0"
log = "0.4"
env_logger = "0.6"
hlua-badtouch = "0.4"
@@ -35,7 +42,7 @@ chrono = { version = "0.4", features = ["serde"] }
dirs = "1.0"
url = "1.7"
#chrootable-https = { path = "../chrootable-https" }
chrootable-https = "0.8"
chrootable-https = "0.10"
base64 = "0.10"
kuchiki = "0.7.2"
serde_urlencoded = "0.5"
@@ -44,12 +51,12 @@ serde_derive = "1.0"
serde_json = "1.0"
crossbeam-channel = "0.3"
ctrlc = "3.1"
opener = "0.3.0"
opener = "0.4"
separator = "0.4"
maplit = "1.0.1"
sloppy-rfc4880 = "0.1.2"
regex = "1.0"
toml = "0.4"
toml = "0.5"
maxminddb = "0.13"
tar = "0.4.17"
libflate = "0.1.14"
@@ -60,11 +67,26 @@ nom = "4.1.1"
atty = "0.2"
bufstream = "0.1.4"
tokio = "0.1.14"
semver = "0.9"
bytes = "0.4"
digest = "0.8.0"
hex = "0.3.1"
bs58 = "0.2.2"
blake2 = "0.8.0"
md-5 = "0.8.0"
sha-1 = "0.8.1"
sha2 = "0.8.0"
image = "0.21"
kamadak-exif = "0.3.1"
walkdir = "2.2"
nude = "0.1.0"
[target.'cfg(target_os="linux")'.dependencies]
caps = "0.3"
#syscallz = { path="../syscallz-rs" }
syscallz = "0.10"
syscallz = "0.11"
nix = "0.13"
[target.'cfg(target_os="openbsd")'.dependencies]
@@ -73,4 +95,5 @@ unveil = "0.2.0"
[dev-dependencies]
#boxxy = { path = "../boxxy-rs" }
boxxy = "0.8"
boxxy = "0.10"
tempfile = "3.0"

View File

@@ -1,13 +1,14 @@
FROM alpine:edge
RUN apk add --no-cache sqlite-dev libseccomp-dev
RUN apk add --no-cache --virtual .build-rust rust cargo
FROM rust
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/sn0int
COPY . .
RUN cargo build --release --verbose
RUN strip target/release/sn0int
FROM alpine:edge
RUN apk add --no-cache libgcc sqlite-libs libseccomp
FROM debian
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=0 /usr/src/sn0int/target/release/sn0int /usr/local/bin/sn0int
VOLUME ["/data", "/cache"]
ENV XDG_DATA_HOME=/data \

View File

@@ -19,3 +19,10 @@ update:
get-oui -v -u http://standards-oui.ieee.org/oui/oui.txt -f data/ieee-oui.txt
get-iab -v -u http://standards-oui.ieee.org/iab/iab.txt -f data/ieee-iab.txt
rm -f data/ieee-*.txt.bak
docs:
$(MAKE) -C docs html
contrib/html-toc2md.pl README.md docs/_build/html/index.html > README2.md
mv README2.md README.md
.PHONY: check force-check test update docs

View File

@@ -1,4 +1,4 @@
# sn0int [![Build Status][travis-img]][travis] [![Crates.io][crates-img]][crates] [![Documentation Status][docs-img]][docs]
# sn0int [![Build Status][travis-img]][travis] [![crates.io][crates-img]][crates] [![Documentation Status][docs-img]][docs] [![irc.hackint.org:6697/#sn0int][irc-img]][irc] [![@sn0int@chaos.social][mastodon-img]][mastodon] [![registry status][registry-img]][registry]
[travis-img]: https://travis-ci.org/kpcyrd/sn0int.svg?branch=master
[travis]: https://travis-ci.org/kpcyrd/sn0int
@@ -6,6 +6,12 @@
[crates]: https://crates.io/crates/sn0int
[docs-img]: https://readthedocs.org/projects/sn0int/badge/?version=latest
[docs]: https://sn0int.readthedocs.io/en/latest/?badge=latest
[irc-img]: https://img.shields.io/badge/hackint-%23sn0int-blue.svg
[irc]: https://webirc.hackint.org/#irc://irc.hackint.org/#sn0int
[mastodon-img]: https://img.shields.io/badge/mastodon-chaos.social-blue.svg
[mastodon]: https://chaos.social/@sn0int
[registry-img]: https://img.shields.io/website/https/sn0int.com.svg?label=registry
[registry]: https://sn0int.com/
sn0int is a semi-automatic OSINT framework and package manager. It was built
for IT security professionals and bug hunters to gather intelligence about a
@@ -15,27 +21,42 @@ unified format for followup investigations.
Among other things, sn0int is currently able to:
- Harvest subdomains from certificate transparency logs
- Harvest subdomains from various passive dns logs
- Sift through subdomain results for publicly accessible websites
- Harvest emails from pgp keyservers
- Enrich ip addresses with ASN and geoip info
- Harvest subdomains from the wayback machine
- Harvest subdomains from certificate transparency logs and passive dns
- Enrich ip addresses with asn and geoip info
- Harvest emails from pgp keyservers and whois
- Discover compromised logins in breaches
- Find somebody's profiles across the internet
- Enumerate local networks with unique techniques like passive arp
- Gather information about phonenumbers
- Bruteforce interesting urls
- Harvest data and images from instagram profiles
- Scan images for nudity
sn0int is heavily inspired by recon-ng and maltego, but remains more flexible
and is fully opensource. None of the investigations listed above are hardcoded
and is fully opensource. None of the investigations listed above are hardcoded
in the source, instead those are provided by modules that are executed in a
sandbox. You can easily extend sn0int by writing your own modules and share
them with other users by publishing them to the sn0int registry. This allows
you to ship updates for your modules on your own since you don't need to send a
pull request.
Join us on IRC: [irc.hackint.org:6697/#sn0int](https://webirc.hackint.org/#irc://irc.hackint.org/#sn0int)
For questions and support join us on IRC: [irc.hackint.org:6697/#sn0int](https://webirc.hackint.org/#irc://irc.hackint.org/#sn0int)
[![asciicast](https://asciinema.org/a/shZ3TVY1o0opGFln3Oi2DAMCB.svg)](https://asciinema.org/a/shZ3TVY1o0opGFln3Oi2DAMCB)
## Installation
Archlinux
pacman -S sn0int
Mac OSX
brew install sn0int
For everything else please have a look at the [detailed list][1].
[1]: https://sn0int.readthedocs.io/en/latest/install.html
## Getting started
- [Installation](https://sn0int.readthedocs.io/en/latest/install.html)
@@ -61,12 +82,30 @@ Join us on IRC: [irc.hackint.org:6697/#sn0int](https://webirc.hackint.org/#irc:/
- [db_add](https://sn0int.readthedocs.io/en/latest/database.html#db-add)
- [db_update](https://sn0int.readthedocs.io/en/latest/database.html#db-update)
- [db_select](https://sn0int.readthedocs.io/en/latest/database.html#db-select)
- [Structs](https://sn0int.readthedocs.io/en/latest/structs.html)
- [Domains](https://sn0int.readthedocs.io/en/latest/structs.html#domains)
- [Subdomains](https://sn0int.readthedocs.io/en/latest/structs.html#subdomains)
- [IpAddrs](https://sn0int.readthedocs.io/en/latest/structs.html#ipaddrs)
- [URLs](https://sn0int.readthedocs.io/en/latest/structs.html#urls)
- [Emails](https://sn0int.readthedocs.io/en/latest/structs.html#emails)
- [Phonenumbers](https://sn0int.readthedocs.io/en/latest/structs.html#phonenumbers)
- [Devices](https://sn0int.readthedocs.io/en/latest/structs.html#devices)
- [Networks](https://sn0int.readthedocs.io/en/latest/structs.html#networks)
- [Accounts](https://sn0int.readthedocs.io/en/latest/structs.html#accounts)
- [Breaches](https://sn0int.readthedocs.io/en/latest/structs.html#breaches)
- [Images](https://sn0int.readthedocs.io/en/latest/structs.html#images)
- [Relations](https://sn0int.readthedocs.io/en/latest/structs.html#relations)
- [subdomain_ipaddr](https://sn0int.readthedocs.io/en/latest/structs.html#subdomain-ipaddr)
- [network_device](https://sn0int.readthedocs.io/en/latest/structs.html#network-device)
- [breach_email](https://sn0int.readthedocs.io/en/latest/structs.html#breach-email)
- [Keyring](https://sn0int.readthedocs.io/en/latest/keyring.html)
- [Managing the keyring](https://sn0int.readthedocs.io/en/latest/keyring.html#managing-the-keyring)
- [Using access keys in scripts](https://sn0int.readthedocs.io/en/latest/keyring.html#using-access-keys-in-scripts)
- [Using access keys as source argument](https://sn0int.readthedocs.io/en/latest/keyring.html#using-access-keys-as-source-argument)
- [Configuration](https://sn0int.readthedocs.io/en/latest/config.html)
- [Configuring a proxy](https://sn0int.readthedocs.io/en/latest/config.html#configuring-a-proxy)
- [\[core\]](https://sn0int.readthedocs.io/en/latest/config.html#core)
- [\[namespaces\]](https://sn0int.readthedocs.io/en/latest/config.html#namespaces)
- [\[network\]](https://sn0int.readthedocs.io/en/latest/config.html#network)
- [Sandbox](https://sn0int.readthedocs.io/en/latest/sandbox.html)
- [Linux](https://sn0int.readthedocs.io/en/latest/sandbox.html#linux)
- [OpenBSD](https://sn0int.readthedocs.io/en/latest/sandbox.html#openbsd)
@@ -75,6 +114,7 @@ Join us on IRC: [irc.hackint.org:6697/#sn0int](https://webirc.hackint.org/#irc:/
- [Diagnosing a sandbox failure](https://sn0int.readthedocs.io/en/latest/sandbox.html#diagnosing-a-sandbox-failure)
- [Function reference](https://sn0int.readthedocs.io/en/latest/reference.html)
- [clear_err](https://sn0int.readthedocs.io/en/latest/reference.html#clear-err)
- [create_blob](https://sn0int.readthedocs.io/en/latest/reference.html#create-blob)
- [datetime](https://sn0int.readthedocs.io/en/latest/reference.html#datetime)
- [db_add](https://sn0int.readthedocs.io/en/latest/reference.html#db-add)
- [db_add_ttl](https://sn0int.readthedocs.io/en/latest/reference.html#db-add-ttl)
@@ -89,18 +129,25 @@ Join us on IRC: [irc.hackint.org:6697/#sn0int](https://webirc.hackint.org/#irc:/
- [http_mksession](https://sn0int.readthedocs.io/en/latest/reference.html#http-mksession)
- [http_request](https://sn0int.readthedocs.io/en/latest/reference.html#http-request)
- [http_send](https://sn0int.readthedocs.io/en/latest/reference.html#http-send)
- [img_load](https://sn0int.readthedocs.io/en/latest/reference.html#img-load)
- [img_exif](https://sn0int.readthedocs.io/en/latest/reference.html#img-exif)
- [img_nudity](https://sn0int.readthedocs.io/en/latest/reference.html#img-nudity)
- [info](https://sn0int.readthedocs.io/en/latest/reference.html#info)
- [json_decode](https://sn0int.readthedocs.io/en/latest/reference.html#json-decode)
- [json_decode_stream](https://sn0int.readthedocs.io/en/latest/reference.html#json-decode-stream)
- [json_encode](https://sn0int.readthedocs.io/en/latest/reference.html#json-encode)
- [keyring](https://sn0int.readthedocs.io/en/latest/reference.html#keyring)
- [last_err](https://sn0int.readthedocs.io/en/latest/reference.html#last-err)
- [md5](https://sn0int.readthedocs.io/en/latest/reference.html#md5)
- [pgp_pubkey](https://sn0int.readthedocs.io/en/latest/reference.html#pgp-pubkey)
- [pgp_pubkey_armored](https://sn0int.readthedocs.io/en/latest/reference.html#pgp-pubkey-armored)
- [print](https://sn0int.readthedocs.io/en/latest/reference.html#print)
- [psl_domain_from_dns_name](https://sn0int.readthedocs.io/en/latest/reference.html#psl-domain-from-dns-name)
- [regex_find](https://sn0int.readthedocs.io/en/latest/reference.html#regex-find)
- [regex_find_all](https://sn0int.readthedocs.io/en/latest/reference.html#regex-find-all)
- [sha1](https://sn0int.readthedocs.io/en/latest/reference.html#sha1)
- [sha2_256](https://sn0int.readthedocs.io/en/latest/reference.html#sha2-256)
- [sha2_512](https://sn0int.readthedocs.io/en/latest/reference.html#sha2-512)
- [sleep](https://sn0int.readthedocs.io/en/latest/reference.html#sleep)
- [sock_connect](https://sn0int.readthedocs.io/en/latest/reference.html#sock-connect)
- [sock_send](https://sn0int.readthedocs.io/en/latest/reference.html#sock-send)
@@ -125,6 +172,23 @@ Join us on IRC: [irc.hackint.org:6697/#sn0int](https://webirc.hackint.org/#irc:/
- [utf8_decode](https://sn0int.readthedocs.io/en/latest/reference.html#utf8-decode)
- [x509_parse_pem](https://sn0int.readthedocs.io/en/latest/reference.html#x509-parse-pem)
## Rationale
This tool was written for companies to help them understand their attack
surface from a blackbox point of view. It's often difficult to understand that
something is easier to discover than some people assume, putting them at risk
of false security.
It's also designed to be useful for red team assessments and bug bounties,
which also help companies to identify weaknesses that could result in a
compromise.
Some functionality was written to do the same thing for individuals to raise
awareness about personal attack surface, privacy and how much data is publicly
available. These issues are often out of scope in bug bounties and sometimes by
design. We believe that blaming the user is the wrong approach and these issues
should be addressed at the root cause by the people designing those systems.
## License
GPLv3+

39
contrib/html-toc2md.pl Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env perl
use strict; use warnings;
my ($readme, $toc) = @ARGV;
defined $readme or die 'missing readme path';
defined $toc or die 'missing toc path';
open(my $r, "<$readme") or die 'failed to open readme';
open(my $t, "<$toc") or die 'failed to open toc';
my $re = qr/^\s*- \[.+\]\(https:\/\/sn0int.readthedocs.io\/en\/.+\)$/;
# pass through start of readme
while (<$r>) {
last if ($_ =~ $re);
print $_;
}
# skip toc
while (<$r>) {
last unless ($_ =~ $re);
}
# generate new toc
while (my $line = <$t>) {
if ($line =~ /toctree-l(\d).*href="([^"]+)">(.+)<\/a/) {
my $space = $1;
my $section = $2;
my $label = $3;
$label =~ s/([\[\]])/\\$1/g;
print $space==2?" ":"", "- [$label](https://sn0int.readthedocs.io/en/latest/$section)\n";
}
}
print;
# pass through end of readme
while (<$r>) {
print $_;
}

View File

@@ -1,2 +0,0 @@
#!/bin/sh
perl -n -e '/toctree-l(\d).*href="([^"]+)">(.+)<\/a/ && print $1==2?" ":"", "- [$3](https://sn0int.readthedocs.io/en/latest/$2)\n"' < docs/_build/html/index.html

View File

@@ -1,17 +1,54 @@
Configuration
=============
This file documents the config file at ``~/.config/sn0int.toml``. By default
this file does not exist and a default configuration is used instead.
This section documents the config file. By default this file does not exist and
a default configuration is used instead.
Configuring a proxy
-------------------
Linux/BSD
``~/.config/sn0int.toml``
OSX
``~/Library/Preferences/sn0int.toml``
Windows
``%APPDATA%/sn0int.toml``
[core]
------
``registry``
Configure the registry you want to use. Defaults to ``https://sn0int.com``.
``no-autoupdate``
sn0int is going to check if your modules are outdated during startout once
a week. Set this option to ``true`` to disable this.
[namespaces]
------------------
By default sn0int modules are assumed to be installed from the registry. You
may want to keep a local directory with private modules, especially during
development. You can configure a folder that contains modules that aren't
managed by sn0int by adding a namespace section to the config file::
[namespaces]
foo = "/opt/sn0int/foo"
bar = "~/repos/a/b/c/sn0int-modules"
This is going to load modules from these two folders and register them in the
``foo`` and ``bar`` namespace.
Note that sn0int is also going to assume that symlinks in
``~/.local/share/sn0int/modules`` and folders containing a ``.git`` folder are
externally managed.
[network]
---------
To enable a proxy, add the following to your config file::
[network]
proxy = "127.0.0.1:9050"
This forces everything through tor and restricts all other functions that
depend on the network. For example the ``dns`` function is fully disabled if a
proxy is configured.
This forces everything through tor (or any other socks5 proxy) and restricts
all other functions that depend on the network. For example the ``dns``
function is fully disabled if a proxy is configured.

View File

@@ -39,6 +39,7 @@ Getting Started
usage
scripting
database
structs
keyring
config
sandbox

View File

@@ -20,9 +20,13 @@ Mac OSX
Debian testing/Debian sid/Kali
------------------------------
Note that debian `doesn't ship the geoip2-database
<https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=757723>`_ so we're going to
download them automatically during the first run.
.. code-block:: bash
$ apt install build-essential cargo libsqlite3-dev libseccomp-dev
$ apt install build-essential cargo libsqlite3-dev libseccomp-dev publicsuffix
$ git clone https://github.com/kpcyrd/sn0int.git
$ cd sn0int
$ cargo install -f --path .
@@ -37,7 +41,7 @@ instructions instead.
.. code-block:: bash
$ apt install build-essential libsqlite3-dev libseccomp-dev
$ apt install build-essential libsqlite3-dev libseccomp-dev publicsuffix
$ git clone https://github.com/kpcyrd/sn0int.git
$ cd sn0int
$ cargo install -f --path .
@@ -64,7 +68,7 @@ OpenBSD
.. code-block:: bash
$ pkg_add sqlite3
$ pkg_add git cargo sqlite3 geolite2-city geolite2-asn
$ git clone https://github.com/kpcyrd/sn0int.git
$ cd sn0int
$ cargo install -f --path .

View File

@@ -52,8 +52,8 @@ If the user granted us access to those keys we can read them with ``keyring``:
.. code-block:: lua
creds = keyring('aws')
print(creds[1]['accesskey'])
print(creds[1]['secretkey'])
debug(creds[1]['access_key'])
debug(creds[1]['secret_key'])
This returns a list of all keys in that namespace. Any empty list is returned
if the user doesn't have any keys in that namespace.

View File

@@ -13,6 +13,18 @@ Clear the last recorded error from the internal state. See also last_err_.
clear_err()
end
create_blob
-----------
Push a byte array into persistent blob storage. This allows passing those bytes
to functions operating on blob storage. Returns a blob identifier that is
deterministic based on the blob content. Blobs are immutable.
.. code-block:: lua
blob = create_blob("some bytes")
debug(blob)
datetime
--------
@@ -210,6 +222,9 @@ options are set. The following options are available:
The request body that should be form encoded.
``body``
The raw request body as string.
``into_blob``
If true, the response body is stored in blob storage and a blob reference is
returned as ``blob`` instead of the full body.
This function may fail.
@@ -237,6 +252,9 @@ the following keys:
A table of headers
``text``
The response body as string
``blob``
If ``into_blob`` was enabled for the request the body is downloaded into blob
storage with a reference to the body in this field.
.. code-block:: lua
@@ -250,6 +268,41 @@ the following keys:
if last_err() then return end
if resp["status"] ~= 200 then return "invalid status code" end
img_load
--------
Attempt to decode a blob as an image and return some basic metadata like the
mime type, height and width.
.. code-block:: lua
img = img_load(blob)
if last_err() then return end
debug(img)
img_exif
--------
Extract exif metadata from an image.
.. code-block:: lua
exif = img_exif(blob)
if last_err() then return end
debug(exif)
img_nudity
----------
Classify an image for nudity. The score goes from 0 to 2. A score above 1 means
nudity has been detected.
.. code-block:: lua
nudity = img_nudity(blob)
if last_err() then return end
debug(nudity)
info
----
@@ -318,6 +371,15 @@ otherwise.
return
end
md5
---
Hash a byte array with md5 and return the results as bytes.
.. code-block:: lua
hex(md5("\x00\xff"))
pgp_pubkey
----------
@@ -390,12 +452,12 @@ psl_domain_from_dns_name
------------------------
Returns the parent domain according to the public suffix list. For
``www.a.b.c.d.example.com`` this is going to be ``example.com``.
``www.a.b.c.d.example.co.uk`` this is going to be ``example.co.uk``.
.. code-block:: lua
domain = psl_domain_from_dns_name('www.a.b.c.d.example.com')
print(domain == 'example.com')
domain = psl_domain_from_dns_name('www.a.b.c.d.example.co.uk')
print(domain == 'example.co.uk')
regex_find
----------
@@ -430,6 +492,33 @@ Same as regex_find_, but returns all matches.
print(m[3][1] == 'ef')
print(m[3][2] == 'f')
sha1
----
Hash a byte array with sha1 and return the results as bytes.
.. code-block:: lua
hex(sha1("\x00\xff"))
sha2_256
--------
Hash a byte array with sha2_256 and return the results as bytes.
.. code-block:: lua
hex(sha2_256("\x00\xff"))
sha2_512
--------
Hash a byte array with sha2_512 and return the results as bytes.
.. code-block:: lua
hex(sha2_512("\x00\xff"))
sleep
-----

View File

@@ -13,7 +13,19 @@ free to change that to something else::
$ git init ~/repos/sn0int-modules
$ cd ~/repos/sn0int-modules
$ ln -s "$PWD" ~/.local/share/sn0int/modules/$YOUR_GITHUB_NAME
We need to add this folder to the sn0int config file so it's correctly detected
when starting sn0int. Open the `config file <config.html>`_ in your prefered
editor. Note that the file does not exist by default and the path is different
depending on your operating system. On linux you would open the config file
with::
$ vim ~/.config/sn0int.toml
Add the follwing::
[namespaces]
your_github_name = "~/repos/sn0int-modules"
Every module we're adding to ``~/repos/sn0int-modules`` is now going to be
picked up by sn0int.

254
docs/structs.rst Normal file
View File

@@ -0,0 +1,254 @@
Structs
=======
This section describes all supported structs in depth. Please refer to this
section if in doubt about the correct usage of fields to ensure
interoperability between modules.
Domains
-------
Represents a registerable domain as defined by the `public suffix list
<https://publicsuffix.org/>`_. If in doubt check `psl_domain_from_dns_name
<reference.html#psl-domain-from-dns-name>`_.
``value``
The domain name, like ``example.co.uk``.
Subdomains
----------
A subdomain of a `domain <#domains>`_. The depth is arbitrary, so
``foo.example.co.uk`` and ``foo.bar.example.co.uk`` are both valid subdomains
of ``example.co.uk``.
``value``
The subdomain, like ``foo.bar.example.co.uk``.
``domain_id``
The numeric id of a domain struct.
``resolvable``
Whether the subdomain can be resolved to a A/AAAA record. nil if unknown.
IpAddrs
-------
An ip address. Note that most of these fields are geoip related and an
approximation instead of an actual location.
``value``
The ip address.
``family``
The address family of the ip address, either ``4`` or ``6``.
``continent``
The continent associated with this ip address.
``continent_code``
The continent code of the ``continent`` field, eg ``NA``.
``country``
The country associated with this ip address.
``country_code``
The country code of the ``country`` field, eg ``US``.
``city``
The city associated with this ip address.
``latitude``
Latitude associated with this ip address.
``longitude``
Longitude associated with this ip address.
``asn``
The number of the autonomous system this ip belongs to.
``as_org``
The organization of the autonomous system this ip belongs to.
``description``
This field is sn0int internal if we have additional information about this
ip address, for example technical identifiers from aws.
``reverse_dns``
The reverse dns name setup for this ip address.
URLs
----
``subdomain_id``
The numeric id of a subdomain struct.
``value``
The url, including a schema, hostname and path.
``status``
The http status code, like ``200``.
``body``
The raw response body. This can be any mime type.
``online``
Whether or not the url gives a http response (even if it's an error).
``title``
The parsed ``<title>`` of the page, if available.
``redirect``
If the server replied with a redirect, this is the url it redirected to.
Emails
------
``value``
The email address.
``displayname``
The display name of a given email address: ``this is the name <foo@example.com>``.
``valid``
Whether that email address is valid or has been disabled.
Phonenumbers
------------
``value``
The phone number in E.164 format (+491234567)
``name``
An alias we can assign to this phone number. This alias is sn0int internal.
``valid``
Whether the number is assigned to a customer.
``last_online``
The last time this number has been online.
``country``
The country this number is associated with.
``carrier``
The name of the carrier this numer is registered with.
``line``
The type of the phone number, can be ``landline``, ``mobile`` or ``voip``.
``is_ported``
Whether this number has been ported to a different carrier.
``last_ported``
The last time this number has been ported.
``caller_name``
The name of the owner of the phone number.
``caller_type``
The type of caller, eg ``business`` or ``consumer``.
Devices
-------
``value``
The devices mac address or another identifier if needed.
``name``
An alias we can assign to this device. This alias is sn0int internal.
``hostname``
The hostname configured on the device.
``vendor``
The hardware vendor of the device. This is usually derived from the mac
address.
``last_seen``
The last time we've observed the device somewhere.
Networks
--------
A wired or wireless network at a specific location that a device could be
connected to.
``value``
The network name. This can be an ssid or any other identifier but should be
unique.
``latitude``
Latitude of the networks location.
``longitude``
Longitude of the networks location.
Accounts
--------
A users account or profile on a webservice, like github or instagram.
``service``
The identifier of the service/website. It's recommended to use the websites
domain for this as defined in `Domains`_.
``username``
The users unique identifier, like the login name. If the login name is not
known or the system doesn't use login names, use the email address instead.
``displayname``
The users display name. This name is often not unique and may contain the
users real name.
``email``
The email address associated with the account.
``url``
The url of the public profile if available.
``last_seen``
The last time this account has been active/online.
Breaches
--------
Either a breach of a specific website, a breach compilation or a breach
notification service.
``value``
The name of the breach, breach compilation or notification service.
Images
------
``value``
The id that identifies the blob. This id is deterministic based on file
content.
``filename``
This field is used if we have a well known filename for the content.
``mime``
The image mimetype, like ``image/png`` or ``image/jpeg``.
``width``
The width of the image.
``height``
The height of the image.
``created``
The date and time this image has been taken.
``latitude``
Latitude this picture has been taken.
``longitude``
Longitude this picture has been taken.
``nudity``
A score that classifies nudity in this picture. The score goes from 0 to 2
and is commonly calculated with ``img_nudity``. A score above 1 means
nudity has been detected.
``ahash``
The Mean (aHash) perceptual hash.
``dhash``
The Gradient (dHash) perceptual hash.
``phash``
The DCT (pHash) perceptual hash.
Relations
---------
Relations are linking two structs together. The link may contain additional information.
subdomain_ipaddr
~~~~~~~~~~~~~~~~
Links an ip address to a subdomain.
``subdomain_id``
The numeric id of a subdomain struct.
``ip_addr_id``
The numeric id of an ip addr struct.
network_device
~~~~~~~~~~~~~~
Links a device to a network. This is commonly used with ``db_add_ttl`` so the
link automatically expires. This is frequently used to monitor networks for
known and unknown devices.
``network_id``
The numeric id of a network struct.
``device_id``
The numeric id of a device struct.
``ipaddr``
The ip address assigned to the device.
``last_seen``
The last time we've seen the device on that network.
breach_email
~~~~~~~~~~~~
Links an email to a breach. If we know the password as well we can add it to
the link. If we don't know the password we can leave it blank and fill it
later. An email can be linked to a breach multiple times with different
passwords. There is a special upserting logic in place to support this.
``breach_id``
The numeric id of a breach struct.
``email_id``
The numeric id of an email struct.
``password``
The password for that email in the breach.

View File

@@ -0,0 +1 @@
DROP TABLE accounts;

View File

@@ -0,0 +1,12 @@
CREATE TABLE accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
value VARCHAR NOT NULL,
service VARCHAR NOT NULL,
username VARCHAR NOT NULL,
displayname VARCHAR,
email VARCHAR,
url VARCHAR,
last_seen DATETIME,
unscoped BOOLEAN DEFAULT 0 NOT NULL,
CONSTRAINT account_unique UNIQUE (value)
);

View File

@@ -0,0 +1,2 @@
DROP TABLE breach_emails;
DROP TABLE breaches;

View File

@@ -0,0 +1,16 @@
CREATE TABLE breaches (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
value VARCHAR NOT NULL,
unscoped BOOLEAN DEFAULT 0 NOT NULL,
CONSTRAINT breach_unique UNIQUE (value)
);
CREATE TABLE breach_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
breach_id INTEGER NOT NULL,
email_id INTEGER NOT NULL,
password VARCHAR,
FOREIGN KEY(breach_id) REFERENCES breaches(id) ON DELETE CASCADE,
FOREIGN KEY(email_id) REFERENCES emails(id) ON DELETE CASCADE,
CONSTRAINT breach_emails_unique UNIQUE (breach_id, email_id, password)
);

View File

@@ -0,0 +1 @@
DROP TABLE images;

View File

@@ -0,0 +1,21 @@
CREATE TABLE images (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
value VARCHAR NOT NULL,
filename VARCHAR,
mime VARCHAR,
width INT,
height INT,
created DATETIME,
latitude FLOAT,
longitude FLOAT,
nudity FLOAT,
ahash VARCHAR,
dhash VARCHAR,
phash VARCHAR,
unscoped BOOLEAN DEFAULT 0 NOT NULL,
CONSTRAINT image_unique UNIQUE (value)
);

View File

@@ -0,0 +1,18 @@
PRAGMA foreign_keys=off;
CREATE TABLE _emails_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
value VARCHAR NOT NULL,
unscoped BOOLEAN DEFAULT 0 NOT NULL,
valid BOOLEAN,
CONSTRAINT email_unique UNIQUE (value)
);
INSERT INTO _emails_new (id, value, unscoped, valid)
SELECT id, value, unscoped, valid
FROM emails;
DROP TABLE emails;
ALTER TABLE _emails_new RENAME TO emails;
PRAGMA foreign_keys=on;

View File

@@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE emails ADD COLUMN displayname VARCHAR;

View File

@@ -1,10 +1,10 @@
-- Description: Try a zone transfer for subdomains
-- Version: 0.2.0
-- Version: 0.3.0
-- Source: domains
-- License: GPL-3.0
function strip_root_dot(name)
m = regex_find("(.+)\\.$", name)
local m = regex_find("(.+)\\.$", name)
if last_err() then return end
if m == nil then
@@ -15,21 +15,19 @@ function strip_root_dot(name)
end
function add_pointer(name)
local domain, domain_id, subdomain_id
-- select psl+1
domain = psl_domain_from_dns_name(name)
local domain = psl_domain_from_dns_name(name)
if last_err() then return end
-- add domain
domain_id = db_add('domain', {
local domain_id = db_add('domain', {
value=domain,
})
if last_err() then return end
if domain_id == nil then return end
-- add subdomain
subdomain_id = db_add('subdomain', {
local subdomain_id = db_add('subdomain', {
domain_id=domain_id,
value=name,
})
@@ -37,26 +35,24 @@ function add_pointer(name)
end
function iter_axfr(zone, arg)
local name, r, m, domain
debug(arg)
name = arg[1]
r = arg[2]
local name = arg[1]
local r = arg[2]
-- select psl+1
domain = psl_domain_from_dns_name(name)
local domain = psl_domain_from_dns_name(name)
if last_err() then return end
-- add domain
domain_id = db_add('domain', {
local domain_id = db_add('domain', {
value=domain,
})
if last_err() then return end
if domain_id == nil then return end
-- add subdomain
subdomain_id = db_add('subdomain', {
local subdomain_id = db_add('subdomain', {
domain_id=domain_id,
value=name,
})
@@ -65,7 +61,7 @@ function iter_axfr(zone, arg)
-- this is a A record
if r['A'] ~= nil then
-- add the name and ip
ipaddr_id = db_add('ipaddr', {
local ipaddr_id = db_add('ipaddr', {
family='4',
value=r['A'],
})
@@ -98,12 +94,10 @@ function iter_axfr(zone, arg)
end
function iter_a(zone, arg)
local i, records, r
if arg == nil then return end
debug('nameserver: ' .. arg)
records = dns(zone, {
local records = dns(zone, {
record='AXFR',
nameserver=arg .. ':53',
tcp=true,
@@ -112,48 +106,40 @@ function iter_a(zone, arg)
if records['error'] ~= nil then return end
records = records['answers']
i = 1
while records[i] ~= nil do
for i=1, #records do
iter_axfr(zone, records[i])
if last_err() then return end
i = i+1
end
end
function iter_ns(zone, arg)
local i, records, r
if arg == nil then return end
records = dns(arg, {
local records = dns(arg, {
record='A',
})
if last_err() then return end
if records['error'] ~= nil then return end
records = records['answers']
i = 1
while records[i] ~= nil do
for i=1, #records do
r = records[i][2]
iter_a(zone, r['A'])
if last_err() then return end
i = i+1
end
end
function run(arg)
records = dns(arg['value'], {
local records = dns(arg['value'], {
record='NS',
})
if last_err() then return end
if records['error'] ~= nil then return end
records = records['answers']
i = 1
while records[i] ~= nil do
r = records[i][2]
for i=1, #records do
local r = records[i][2]
iter_ns(arg['value'], r['NS'])
if last_err() then return end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query for CNAMES to find subdomains
-- Version: 0.2.0
-- Version: 0.3.0
-- Source: subdomains
-- License: GPL-3.0
@@ -40,11 +40,9 @@ function run(arg)
if records['error'] ~= nil then return end
records = records['answers']
i = 1
while records[i] ~= nil do
for i=1, #records do
r = records[i][2]
iter(r['CNAME'])
if last_err() then return end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query certificate transparency logs to discover subdomains
-- Version: 0.4.0
-- Version: 0.5.0
-- Source: domains
-- License: GPL-3.0
@@ -70,8 +70,7 @@ function run(arg)
seen = {}
i = 1
while i <= #certs do
for i=1, #certs do
c = certs[i]
debug(c)
@@ -92,15 +91,11 @@ function run(arg)
if last_err() then return end
names = crt['valid_names']
j = 1
while j <= #names do
for j=1, #names do
each_name(names[j])
j = j+1
end
else
each_name(c['name_value'])
end
i = i+1
end
end

View File

@@ -0,0 +1,88 @@
-- Description: Export dhcp leases from ddwrt webinterface
-- Version: 0.2.0
-- License: GPL-3.0
function run()
network = getopt('network')
if not network then
return 'network option is missing'
end
network_id = db_select('network', network)
if not network_id then
return 'network not found in database'
end
skip_redacted = not getopt('use-redacted')
router = getopt('router') -- http://192.0.2.1/
if not router then
return 'router option is missing (http://192.0.2.1/)'
end
username = getopt('user')
password = getopt('password')
options = {}
if username and password then
options['basic_auth'] = {username, password}
end
-- request status page
session = http_mksession()
url = url_join(router, '/Info.live.htm')
req = http_request(session, 'GET', url, options)
resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then
return 'http error: ' .. resp['status']
end
txt = resp['text']
debug(txt)
-- extract leases from response
dhcp_section = regex_find('\\{dhcp_leases:: ([^\\}]+)\\}', txt)
if last_err() then return end
if not dhcp_section then
return 'Failed to get dhcp lease section'
end
leases = regex_find_all('\'([^\']+)\',\'([^\']+)\',\'([^\']+)\',\'[^\']+\',\'[^\']+\'', dhcp_section[2])
if last_err() then return end
now = datetime()
-- add devices to database
for i=1, #leases do
local hostname = leases[i][2]
local ipaddr = leases[i][3]
local macaddr = leases[i][4]
debug({
hostname=hostname,
ipaddr=ipaddr,
macaddr=macaddr,
})
if skip_redacted and macaddr:match('^xx:xx:') then
info('Skipping redacted macaddr')
else
local device = {
value=macaddr,
last_seen=now,
}
if hostname ~= '*' then
device['hostname'] = hostname
end
local device_id = db_add('device', device)
db_add_ttl('network-device', {
network_id=network_id,
device_id=device_id,
ipaddr=ipaddr,
last_seen=now,
}, 120)
end
end
end

51
modules/dev/dns-ns.lua Normal file
View File

@@ -0,0 +1,51 @@
-- Description: Add a domains NS records to scope
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: domains
function strip_root_dot(name)
local m = regex_find("(.+)\\.$", name)
if last_err() then return end
if m == nil then
return name
else
return m[2]
end
end
function each(r)
local name = strip_root_dot(r)
local domain = psl_domain_from_dns_name(name)
if last_err() then return end
-- add domain
local domain_id = db_add('domain', {
value=domain,
})
if last_err() then return end
if domain_id == nil then return end
-- add subdomain
local subdomain_id = db_add('subdomain', {
domain_id=domain_id,
value=name,
})
if last_err() then return end
end
function run(arg)
local records = dns(arg['value'], {
record='NS',
})
if last_err() then return end
if records['error'] ~= nil then return end
records = records['answers']
for i=1, #records do
local r = records[i][2]
debug(r)
each(r['NS'])
if last_err() then return end
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Run reverse dns lookups
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: ipaddrs
-- License: GPL-3.0
@@ -17,8 +17,7 @@ function run(arg)
if records['error'] ~= nil then return end
records = records['answers']
i = 1
while records[i] ~= nil do
for i=1, #records do
r = records[i][2]
if r['PTR'] then
db_update('ipaddr', arg, {
@@ -26,7 +25,6 @@ function run(arg)
})
if last_err() then return end
end
i = i+1
end
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query subdomains to discovery ip addresses and verify the record is visible
-- Version: 0.2.0
-- Version: 0.3.0
-- Source: subdomains
-- License: GPL-3.0
@@ -22,8 +22,7 @@ function run(arg)
records = records['answers']
i = 1
while records[i] ~= nil do
for i=1, #records do
r = records[i][2]
if r['A'] ~= nil then
ipaddr_id = db_add('ipaddr', {
@@ -38,6 +37,5 @@ function run(arg)
})
if last_err() then return end
end
i = i+1
end
end

12
modules/dev/exif.lua Normal file
View File

@@ -0,0 +1,12 @@
-- Description: Extract exif data from images
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: images
function run(arg)
exif = img_exif(arg['value'])
if last_err() then return end
debug(exif)
db_update('image', arg, exif)
end

107
modules/dev/github.lua Normal file
View File

@@ -0,0 +1,107 @@
-- Description: Collect data from github profiles
-- Version: 0.2.0
-- Source: accounts:github.com
-- License: GPL-3.0
function api_get(url)
local req = http_request(session, 'GET', url, {})
local resp = http_send(req)
if last_err() then return end
-- TODO: set_error(?)
if resp['status'] == 403 then return 'ratelimit exceeded' end
if resp['status'] ~= 200 then return 'invalid status code' end
local data = json_decode(resp['text'])
if last_err() then return end
return data
end
function import_gpg(url)
local req = http_request(session, 'GET', url, {})
local resp = http_send(req)
if last_err() then return end
local key = pgp_pubkey_armored(resp['text'])
if not key['uids'] then return end
for i=1, #key['uids'] do
local k = key['uids'][i]
debug(k)
local m = regex_find("(.+) <([^< ]+@[^< ]+)>$", k)
if m then
db_add('email', {
value=m[3],
displayname=m[2],
})
end
end
end
function scan4email(username)
local url = 'https://api.github.com/users/' .. username .. '/repos'
local repos = api_get(url)
if last_err() then return end
-- XXX: 'https://api.github.com/users/' .. username .. '/events/public?page=0&per_page=100' is faster but less accurate
for i=1, #repos do
local repo = repos[i]
debug(repo)
local commits = api_get(repo['url'] .. '/commits')
if last_err() then return end
for j=1, #commits do
local commit = commits[j]
debug(commit)
if commit['author'] and commit['author']['login'] == username then
local name = commit['commit']['author']['name']
local email = commit['commit']['author']['email']
db_add('email', {
value=email,
displayname=name,
})
return email
end
if commit['committer'] and commit['committer']['login'] == username then
local name = commit['commit']['committer']['name']
local email = commit['commit']['committer']['email']
db_add('email', {
value=email,
displayname=name,
})
return email
end
end
end
end
function run(arg)
session = http_mksession()
local url = 'https://api.github.com/users/' .. arg['username']
local data = api_get(url)
if last_err() then return end
debug(data)
-- company = data['company']
-- location = data['location']
-- homepage = data['blog']
url = 'https://github.com/' .. arg['username'] .. '.gpg'
import_gpg(url)
if last_err() then return end
local email = data['email']
if not email and not arg['email'] then
email = scan4email(arg['username'])
if last_err() then return end
end
db_update('account', arg, {
url=data['html_url'],
displayname=data['name'],
email=email,
})
end

View File

@@ -1,5 +1,5 @@
-- Description: Query hackertarget for subdomains of a domain
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: domains
-- License: GPL-3.0
@@ -18,13 +18,10 @@ function run(arg)
m = regex_find_all("([^,]+),.+\\n?", resp['text'])
i = 1
while i <= #m do
for i=1, #m do
db_add('subdomain', {
domain_id=arg['id'],
value=m[i][2]
})
i = i+1
end
end

View File

@@ -0,0 +1,56 @@
-- Description: Query device location from home assistant
-- Version: 0.1.0
-- License: GPL-3.0
-- Keyring-Access: home-assistant
function run()
-- parsing options
instance = getopt('instance')
if not instance then
return 'instance option is missing'
end
host = url_parse(instance)
if last_err() then return end
host = host['host']
entity = getopt('entity')
if not entity then
return 'entity option is missing'
end
-- fetching credentials
creds = keyring('home-assistant:' .. host)
if creds[1] == nil then
profile = url_join(instance, 'profile')
return 'missing home-assistant:' .. host .. ' Long-Lived Access Token, open ' .. profile
end
token = creds[1]['secret_key']
headers = {}
headers['Authorization'] = 'Bearer ' .. token
headers['Content-Type'] = 'application/json'
-- requesting status
session = http_mksession()
url = url_join(instance, 'api/states/' .. entity)
req = http_request(session, 'GET', url, {
headers=headers
})
r = http_send(req)
if last_err() then return end
if r['status'] ~= 200 then
return 'http error: ' .. r['status']
end
m = json_decode(r['text'])
if last_err() then return end
debug(m)
info({
gps_accuracy=m['attributes']['gps_accuracy'],
longitude=m['attributes']['longitude'],
latitude=m['attributes']['latitude'],
last_updated=m['last_updated'],
})
end

9
modules/dev/images.lua Normal file
View File

@@ -0,0 +1,9 @@
-- Description: Parse image metadata
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: images
function run(arg)
local img = img_load(arg['value'])
db_update('image', arg, img)
end

150
modules/dev/instagram.lua Normal file
View File

@@ -0,0 +1,150 @@
-- Description: Collect data from instagram profiles
-- Version: 0.2.0
-- Source: accounts:instagram.com
-- License: GPL-3.0
PAGE_SIZE = 50
function get_shared_data(html)
local s = html_select_list(html, 'script')
for i=1, #s do
local m = regex_find('^window\\._sharedData = (.+);$', s[i]['text'])
if m then
return json_decode(m[2])
end
end
end
function sign_request(rhx, json_params)
local magic = rhx .. ':' .. json_params
local x_instagram_gis = hex(md5(magic))
return x_instagram_gis
end
function download_image(node)
local url = node['display_url']
debug(url)
local req = http_request(session, 'GET', url, {
into_blob=true,
})
local r = http_send(req)
if last_err() then return end
if r['status'] ~= 200 then return 'http error: ' .. r['status'] end
db_add('image', {
value=r['blob'],
})
end
function pull_graphql(page)
local end_cursor = page['page_info']['end_cursor']
for i=1, #page['edges'] do
-- shortcode = page['edges'][i]['shortcode']
local node = page['edges'][i]['node']
node['thumbnail_resources'] = nil
node['media_preview'] = nil
-- debug(node)
-- if node['__typename'] == 'GraphImage'
-- node['dimensions']['height']
-- node['dimensions']['width']
-- ^ not sure how to get that picture
-- node['taken_at_timestamp']
-- location = node['location']
local err = download_image(node)
if last_err() then return end
if err ~= nil then return err end
todo_posts = todo_posts -1
debug('posts left: ' .. todo_posts .. '/' .. total_posts)
end
if page['page_info']['has_next_page'] then
debug('requesting next page=' .. end_cursor)
variables = json_encode({
id=user['id'],
first=PAGE_SIZE,
after=end_cursor
})
local headers = {}
headers['X-Instagram-GIS'] = sign_request(rhx_gis, variables)
local req = http_request(session, 'GET', 'https://www.instagram.com/graphql/query/', {
query={
query_hash='42323d64886122307be10013ad2dcc44',
variables=variables,
},
headers=headers,
})
r = http_send(req)
if last_err() then return end
if r['status'] ~= 200 then return 'http error: ' .. r['status'] end
x = json_decode(r['text'])
if last_err() then return end
return pull_graphql(x['data']['user']['edge_owner_to_timeline_media'])
end
end
function run(arg)
session = http_mksession()
local url = 'https://www.instagram.com/' .. arg['username'] .. '/'
local req = http_request(session, 'GET', url, {})
local resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then return 'invalid status code' end
local html = resp['text']
local ld = html_select(html, 'script[type="application/ld+json"]')
if last_err() then return end
local ld = json_decode(ld['text'])
if last_err() then return end
--debug(ld)
if ld['email'] then
db_add('email', {
value=ld['email'],
})
end
-- homepage=ld['url']
db_update('account', arg, {
displayname=ld['name'],
email=ld['email'],
url=url,
})
-- download images
local sd = get_shared_data(html)
if last_err() then return end
-- debug(sd)
rhx_gis = sd['rhx_gis']
user = sd['entry_data']['ProfilePage'][1]['graphql']['user']
-- user['full_name']
-- user['id']
-- user['is_business_account']
-- user['is_private']
-- user['is_verified']
-- user['has_blocked_viewer']
-- user['connected_fb_page']
-- user['country_block']
local page = user['edge_owner_to_timeline_media']
total_posts = page['count']
todo_posts = total_posts
-- TODO: fast-update abort if image has been downloaded already
return pull_graphql(page)
end

View File

@@ -0,0 +1,28 @@
-- Description: Find keybase proofs for domains
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: domains
function run(arg)
session = http_mksession()
req = http_request(session, 'GET', 'https://keybase.io/_/api/1.0/user/lookup.json', {
query={
domain=arg['value'],
}
})
resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then return 'http error: ' .. resp['status'] end
x = json_decode(resp['text'])
if last_err() then return end
debug(x)
if x['them'][1] == nil then return end
them = x['them'][1]
db_add('account', {
service='keybase.io',
username=them['basics']['username'],
})
end

View File

@@ -0,0 +1,44 @@
-- Description: Find keybase proofs for online accounts
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: accounts
function run(arg)
service = arg['service']
if service == 'twitter.com' then
service = 'twitter'
elseif service == 'github.com' then
service = 'github'
elseif service == 'reddit.com' then
service = 'reddit'
elseif service == 'news.ycombinator.com' then
service = 'hackernews'
elseif service == 'facebook.com' then
service = 'facebook'
else
return
end
query = {}
query[service] = arg['username']
session = http_mksession()
req = http_request(session, 'GET', 'https://keybase.io/_/api/1.0/user/lookup.json', {
query=query,
})
resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then return 'http error: ' .. resp['status'] end
x = json_decode(resp['text'])
if last_err() then return end
debug(x)
if x['them'][1] == nil then return end
them = x['them'][1]
db_add('account', {
service='keybase.io',
username=them['basics']['username'],
})
end

85
modules/dev/keybase.lua Normal file
View File

@@ -0,0 +1,85 @@
-- Description: Collect accounts and emails from keybase accounts
-- Version: 0.2.0
-- License: GPL-3.0
-- Source: accounts:keybase.io
function extract_mails(pubkey)
for j=1, #pubkey['uids'] do
local m = regex_find("(.+) <([^< ]+@[^< ]+)>$", pubkey['uids'][j])
if m then
db_add('email', {
value=m[3],
displayname=m[2],
})
end
end
end
function add_domain(dns)
local domain = psl_domain_from_dns_name(dns)
if last_err() then return end
local domain_id = db_add('domain', {
value=domain,
})
if last_err() then return end
if domain_id == nil then return end
if domain ~= dns then
db_add('subdomain', {
domain_id=domain_id,
value=dns,
})
end
end
function run(arg)
session = http_mksession()
req = http_request(session, 'GET', 'https://keybase.io/_/api/1.0/user/lookup.json', {
query={
usernames=arg['username'],
}
})
resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then return 'http error: ' .. resp['status'] end
x = json_decode(resp['text'])
if last_err() then return end
debug(x)
if x['them'][1] == nil then return end
them = x['them'][1]
-- update keybase profile
db_update('account', arg, {
displayname=them['profile']['full_name'],
url='https://keybase.io/'..arg['username'],
})
-- collect emails
pubkey = pgp_pubkey_armored(them['public_keys']['primary']['bundle'])
debug(pubkey)
extract_mails(pubkey)
-- collect profiles
profiles = them['proofs_summary']['all']
for i=1, #profiles do
profile = profiles[i]
debug(profile)
if
profile['proof_type'] == 'generic_web_site' or
profile['proof_type'] == 'dns'
then
add_domain(profile['nametag'])
else
db_add('account', {
service=profile['proof_type'],
username=profile['nametag'],
url=profile['service_url'],
})
end
end
end

85
modules/dev/namechk.lua Normal file
View File

@@ -0,0 +1,85 @@
-- Description: Find accounts by username with namechk.com
-- Version: 0.2.0
-- Source: accounts
-- License: GPL-3.0
function get_services(html)
local divs = html_select_list(html, '.service')
if last_err() then return end
local services = {}
for i=1, #divs do
services[i] = divs[i]['attrs']['data-name']
end
return services
end
function run(arg)
-- setup session
local session = http_mksession()
local req = http_request(session, 'GET', 'https://namechk.com/', {})
local resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then return 'http error: ' .. resp['status'] end
local token = html_select(resp['text'], 'input[name="authenticity_token"]')
local auth_token = token['attrs']['value']
local headers = {}
headers['X-CSRF-Token'] = authenticity_token
local services = get_services(resp['text'])
debug({
auth_token=auth_token,
services=services,
})
-- trigger the scan
local req = http_request(session, 'POST', 'https://namechk.com/', {
headers=headers,
form={
authenticity_token=auth_token,
q=arg['username'],
}
})
local resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then return 'http error: ' .. resp['status'] end
debug(resp)
local scan = json_decode(resp['text'])
if last_err() then return end
local scan_token = scan['valid']
-- get results
for i=1, #services do
debug(services[i])
local req = http_request(session, 'POST', 'https://namechk.com/services/check', {
headers=headers,
form={
token=scan_token,
fat=auth_token,
service=services[i],
}
})
local resp = http_send(req)
if last_err() then return end
if resp['status'] == 200 then
local acc = json_decode(resp['text'])
if last_err() then return end
debug(acc)
if acc ~= nil and not acc['available'] and acc['status'] == 'unavailable' then
db_add('account', {
service=services[i],
username=arg['username'],
url=acc['callback_url'],
})
end
end
end
end

12
modules/dev/nudity.lua Normal file
View File

@@ -0,0 +1,12 @@
-- Description: Scan collected images for nudity
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: images
function run(arg)
local nudity = img_nudity(arg['value'])
debug(nudity)
db_update('image', arg, {
nudity=nudity['score'],
})
end

View File

@@ -1,5 +1,5 @@
-- Description: Query alienvault otx passive dns for subdomains of a domain
-- Version: 0.2.0
-- Version: 0.3.0
-- Source: domains
-- License: GPL-3.0
@@ -18,15 +18,12 @@ function run(arg)
if last_err() then return end
o = o['passive_dns']
i = 1
while o[i] do
for i=1, #o do
x = o[i]
db_add('subdomain', {
domain_id=arg['id'],
value=x['hostname'],
})
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Scrape known http responses for urls
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: urls
-- License: GPL-3.0
@@ -52,12 +52,9 @@ function run(arg)
end
-- process html links
i = 1
while i <= #links do
for i=1, #links do
href = links[i]['attrs']['href']
entry(arg['value'], href)
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query pgp keyserver for email addresses
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: domains
-- License: GPL-3.0
@@ -20,8 +20,7 @@ function run(arg)
if resp['status'] ~= 200 then return 'http error: ' .. resp['status'] end
links = html_select_list(resp['text'], 'a')
i = 1
while i <= #links do
for i=1, #links do
href = links[i]['attrs']['href']
if href:find('/pks/lookup%?op=get&search=') == 1 then
@@ -40,19 +39,16 @@ function run(arg)
-- TODO: ensure at least one email matches our target domain
if pubkey['uids'] then
j = 1
while j <= #pubkey['uids'] do
m = regex_find("<([^< ]+@[^< ]+)>$", pubkey['uids'][j])
for j=1, #pubkey['uids'] do
local m = regex_find("(.+) <([^< ]+@[^< ]+)>$", pubkey['uids'][j])
if m then
db_add('email', {
value=m[2],
value=m[3],
displayname=m[2],
})
end
j = j+1
end
end
end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Search for phpmyadmin
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: urls
-- License: GPL-3.0
@@ -76,8 +76,7 @@ function run(arg)
session = http_mksession()
i = 1
while i <= #paths do
for i=1, #paths do
p = paths[i]
url = url_join(arg['value'], p)
debug(url)
@@ -100,7 +99,5 @@ function run(arg)
})
end
end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Verify email address by asking the smtp server
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: emails
-- License: GPL-3.0
@@ -14,14 +14,12 @@ function find_mx(domain)
records = records['answers']
-- debug(records)
i = 1
while i <= #records do
for i=1, #records do
r = records[i][2]['MX']
if r then
debug('mx: ' .. r[2])
return r[2]
end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query ThreatMiner passive dns for subdomains of an ip address
-- Version: 0.2.0
-- Version: 0.3.0
-- Source: ipaddrs
-- License: GPL-3.0
@@ -23,8 +23,7 @@ function run(arg)
if last_err() then return end
o = o['results']
i = 1
while o[i] do
for i=1, #o do
x = o[i]
domain = psl_domain_from_dns_name(x['domain'])
@@ -46,7 +45,5 @@ function run(arg)
ip_addr_id=arg['id'],
})
end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query ThreatMiner passive dns for subdomains of a domain
-- Version: 0.2.0
-- Version: 0.3.0
-- Source: domains
-- License: GPL-3.0
@@ -21,15 +21,12 @@ function run(arg)
if last_err() then return end
o = o['results']
i = 1
while o[i] do
for i=1, #o do
x = o[i]
db_add('subdomain', {
domain_id=arg['id'],
value=x,
})
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Query thunderbird autoconfig db for subdomains
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: domains
-- License: GPL-3.0
@@ -27,8 +27,7 @@ function run(arg)
m = regex_find_all('<hostname>([^<]+)</hostname>', resp['text'])
i = 1
while i <= #m do
for i=1, #m do
subdomain = m[i][2]
domain = psl_domain_from_dns_name(subdomain)
@@ -42,7 +41,5 @@ function run(arg)
value=subdomain,
})
if last_err() then return end
i = i+1
end
end

46
modules/dev/tld-scan.lua Normal file
View File

@@ -0,0 +1,46 @@
-- Description: Search for the same domain base on all TLDs
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: domains
function run(arg)
local m = regex_find('^([^\\.]+)\\.', arg['value'])
local base = m[2]
-- TODO: we need a way to cache this
-- TODO: .co.uk is missing
local url = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt'
local session = http_mksession()
local req = http_request(session, 'GET', url, {})
local resp = http_send(req)
if last_err() then return end
if resp['status'] ~= 200 then
return 'http error: ' .. resp['status']
end
local tlds = regex_find_all('([^\n]+)', resp['text'])
for i=1, #tlds do
local tld = tlds[i][1]:lower()
if not tld:match('^#') then
local domain = base .. '.' .. tld
debug(domain)
records = dns(domain, {
record='NS',
})
if last_err() then
clear_err()
else
if records['error'] == nil and records['answers'][1] then
debug(records)
db_add('domain', {
value=domain,
})
end
end
end
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Discover subdomains from wayback machine
-- Version: 0.3.0
-- Version: 0.4.0
-- Source: domains
-- License: GPL-3.0
@@ -29,8 +29,7 @@ function run(arg)
seen = {}
i = 2
while o[i] do
for i=2, #o do
url = o[i][3]
debug(url)
parts = url_parse(url)
@@ -51,7 +50,5 @@ function run(arg)
seen[subdomain] = 1
end
end
i = i+1
end
end

View File

@@ -1,5 +1,5 @@
-- Description: Scan for known /.well-known/ locations
-- Version: 0.1.0
-- Version: 0.2.0
-- Source: urls
-- License: GPL-3.0
@@ -24,8 +24,7 @@ function run(arg)
session = http_mksession()
i = 1
while i <= #locations do
for i=1, #locations do
path = locations[i]['path']
expect_redirect = locations[i]['redirect']
@@ -58,7 +57,5 @@ function run(arg)
db_add('url', obj)
end
end
i = i+1
end
end

View File

@@ -0,0 +1,21 @@
-- Description: Download an image
-- Version: 0.1.0
-- License: GPL-3.0
function run()
session = http_mksession()
req = http_request(session, 'GET', 'https://www.kernel.org/theme/images/logos/tux.png', {
into_blob=true,
})
r = http_send(req)
if last_err() then return end
if r['status'] ~= 200 then
return 'http error: ' .. r['status']
end
debug(r)
db_add('image', {
value=r['blob'],
})
end

View File

@@ -8,22 +8,22 @@ repository = "https://github.com/kpcyrd/sn0int"
edition = "2018"
[dependencies]
sn0int-common = { version="0.4.0", path="sn0int-common" }
rocket = "0.4"
rocket_failure = { version = "0.1", features = ["with-rocket"] }
rocket_contrib = { version = "0.4", features = ["handlebars_templates"] }
sn0int-common = { version="0.6.0", path="sn0int-common" }
rocket = { version = "0.4", default-features=false }
rocket_failure = { version = "0.1.2", features = ["with-rocket"] }
rocket_contrib = { version = "0.4.1", features = ["handlebars_templates"] }
diesel = { version = "1.3", features = ["postgres", "r2d2"] }
diesel_migrations = { version = "1.3.0", features = ["postgres"] }
diesel_full_text_search = "1.0.1"
reqwest = "0.9.2"
oauth2 = "2.0.0-alpha.2"
oauth2 = "2.0.0-beta.2"
failure = "0.1"
url = "1.0"
log = "0.4"
semver = "0.9.0"
lazy_static = "1.1"
lazy_static = "1"
blake2 = "0.8.0"
hex = "0.3.1"
maplit = "1.0.1"

View File

@@ -0,0 +1 @@
ALTER TABLE releases DROP COLUMN published;

View File

@@ -0,0 +1 @@
ALTER TABLE releases ADD COLUMN published TIMESTAMP NOT NULL DEFAULT NOW();

View File

@@ -1,6 +1,6 @@
[package]
name = "sn0int-common"
version = "0.4.0"
version = "0.6.0"
description = "Common code for sn0int"
authors = ["kpcyrd <git@rxv.cc>"]
license = "GPL-3.0"

View File

@@ -46,3 +46,8 @@ impl SearchResponse {
format!("{}/{}", self.author, self.name)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LatestResponse {
pub time: Option<u64>,
}

View File

@@ -35,6 +35,11 @@ pub enum Source {
Urls,
Emails,
PhoneNumbers,
Networks,
Devices,
Accounts(Option<String>),
Breaches,
Images,
KeyRing(String),
}
@@ -56,6 +61,11 @@ impl FromStr for Source {
("urls", None) => Ok(Source::Urls),
("emails", None) => Ok(Source::Emails),
("phonenumbers", None) => Ok(Source::PhoneNumbers),
("networks", None) => Ok(Source::Networks),
("devices", None) => Ok(Source::Devices),
("accounts", param) => Ok(Source::Accounts(param.map(String::from))),
("breaches", None) => Ok(Source::Breaches),
("images", None) => Ok(Source::Images),
("keyring", Some(param)) => Ok(Source::KeyRing(param.to_string())),
(x, Some(param)) => bail!("Unknown Source: {:?} ({:?})", x, param),
(x, None) => bail!("Unknown Source: {:?}", x),
@@ -244,4 +254,13 @@ mod tests {
let x = Source::from_str("keyring");
assert!(x.is_err());
}
#[test]
fn verify_account_source() {
let x = Source::from_str("accounts").unwrap();
assert_eq!(x, Source::Accounts(None));
let x = Source::from_str("accounts:github.com").unwrap();
assert_eq!(x, Source::Accounts(Some("github.com".into())));
}
}

View File

@@ -2,7 +2,7 @@ use crate::errors::*;
use diesel::pg::PgConnection;
use oauth2::basic::BasicClient;
use oauth2::prelude::*;
use oauth2::{AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, TokenUrl};
use oauth2::{AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, TokenUrl, TokenResponse};
use crate::github::GithubAuthenticator;
use crate::models::AuthToken;
use url::Url;

View File

@@ -78,12 +78,17 @@ fn run() -> Result<()> {
}))
.mount("/api/v0", routes![
routes::api::quickstart,
routes::api::latest,
routes::api::search,
routes::api::info,
routes::api::download,
routes::api::publish,
routes::api::whoami,
])
.mount("/api/v0/badges", routes![
routes::badges::modules,
routes::badges::downloads,
])
.mount("/auth", routes![
routes::auth::get,
routes::auth::post,
@@ -93,6 +98,7 @@ fn run() -> Result<()> {
routes::assets::index,
routes::assets::favicon,
routes::assets::style,
routes::health::health,
])
.register(catchers![
bad_request,

View File

@@ -4,6 +4,7 @@ use diesel::pg::PgConnection;
use diesel::sql_types::BigInt;
use diesel_full_text_search::{plainto_tsquery, TsQueryExtensions};
use crate::schema::*;
use std::time::SystemTime;
#[derive(AsChangeset, Serialize, Deserialize, Queryable, Insertable)]
@@ -159,7 +160,7 @@ impl Module {
modules::description,
modules::latest,
modules::featured,
diesel::dsl::sql::<BigInt>("sum(releases.downloads) AS sum"),
diesel::dsl::sql::<BigInt>("coalesce(sum(releases.downloads), 0) AS sum"),
))
.left_join(releases::table)
.group_by(modules::id)
@@ -194,6 +195,15 @@ impl Module {
.load(connection)
.map_err(Error::from)
}
pub fn count(connection: &PgConnection) -> Result<i64> {
use diesel::dsl::*;
modules::table
.select(count(modules::id))
.first(connection)
.map_err(Error::from)
}
}
#[derive(Insertable)]
@@ -214,6 +224,7 @@ pub struct Release {
pub version: String,
pub downloads: i32,
pub code: String,
pub published: SystemTime,
}
impl Release {
@@ -271,6 +282,21 @@ impl Release {
.execute(connection)?;
Ok(())
}
pub fn latest(connection: &PgConnection) -> Result<Option<Release>> {
releases::table
.order_by(releases::published.desc())
.first::<Release>(connection)
.optional()
.map_err(Error::from)
}
pub fn downloads(connection: &PgConnection) -> Result<i64> {
releases::table
.select(diesel::dsl::sql::<BigInt>("coalesce(sum(releases.downloads), 0) AS sum"))
.first(connection)
.map_err(Error::from)
}
}
#[derive(Insertable)]

View File

@@ -9,6 +9,7 @@ use semver::Version;
use sn0int_common::api::*;
use sn0int_common::id;
use sn0int_common::metadata::Metadata;
use std::time::UNIX_EPOCH;
#[get("/quickstart")]
@@ -17,6 +18,20 @@ pub fn quickstart(connection: db::Connection) -> ApiResult<ApiResponse<Vec<Modul
Ok(ApiResponse::Success(modules))
}
#[get("/latest")]
pub fn latest(connection: db::Connection) -> ApiResult<ApiResponse<LatestResponse>> {
let time = Release::latest(&connection)?
.map(|x| {
x.published.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
});
let latest = LatestResponse {
time,
};
Ok(ApiResponse::Success(latest))
}
#[derive(Debug, FromForm)]
pub struct Search {
q: String,

View File

@@ -0,0 +1,45 @@
use crate::errors::*;
use crate::db;
use crate::models::*;
use rocket_contrib::json::Json;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Badge {
schema_version: u8,
label: String,
message: String,
color: &'static str,
}
impl Badge {
pub fn new(label: String, message: String) -> Badge {
Badge {
schema_version: 1,
label,
message,
color: "blue",
}
}
}
#[get("/modules")]
pub fn modules(connection: db::Connection) -> ApiResult<Json<Badge>> {
let num = Module::count(&connection)?;
Ok(Json(Badge::new("modules".into(), num.to_string())))
}
#[get("/downloads")]
pub fn downloads(connection: db::Connection) -> ApiResult<Json<Badge>> {
let mut num = Release::downloads(&connection)?;
let mut unit = String::new();
while num > 1000 {
num /= 1000;
unit.push('k');
}
let message = format!("{:.1}{}", num, unit);
Ok(Json(Badge::new("downloads".into(), message)))
}

View File

@@ -0,0 +1,8 @@
use crate::errors::*;
use crate::db;
#[get("/health")]
pub fn health(_connection: db::Connection) -> ApiResult<()> {
Ok(())
}

View File

@@ -1,3 +1,5 @@
pub mod api;
pub mod assets;
pub mod auth;
pub mod badges;
pub mod health;

View File

@@ -34,6 +34,7 @@ table! {
version -> Varchar,
downloads -> Int4,
code -> Text,
published -> Timestamp,
}
}

View File

@@ -46,6 +46,7 @@ impl Client {
info!("Adding session token to request");
request.header("Auth", session.as_str());
}
request.header("User-Agent", web::default_user_agent());
let request = request.body(body)?;
@@ -131,4 +132,10 @@ impl Client {
let reply = self.get::<Vec<ModuleInfoResponse>>(&url)?;
Ok(reply)
}
pub fn latest_release(&self) -> Result<LatestResponse> {
let url = format!("{}/api/v0/latest", self.server);
let reply = self.get::<LatestResponse>(&url)?;
Ok(reply)
}
}

View File

@@ -9,8 +9,8 @@ use crate::workspaces::Workspace;
#[structopt(author = "",
raw(global_settings = "&[AppSettings::ColoredHelp]"))]
pub struct Args {
#[structopt(short="w", long="workspace")]
/// Select a different workspace instead of the default
#[structopt(short="w", long="workspace")]
pub workspace: Option<Workspace>,
#[structopt(subcommand)]
@@ -28,66 +28,75 @@ impl Args {
#[derive(Debug, StructOpt)]
pub enum SubCommand {
#[structopt(author="", name="run")]
/// Run a module directly
#[structopt(author="", name="run")]
Run(Run),
#[structopt(author="", name="sandbox")]
/// For internal use
#[structopt(author="", name="sandbox")]
Sandbox(Sandbox),
#[structopt(author="", name="login")]
/// Login to the registry for publishing
#[structopt(author="", name="login")]
Login(Login),
#[structopt(author="", name="new")]
/// Create a new module
#[structopt(author="", name="new")]
New(New),
#[structopt(author="", name="publish")]
/// Publish a script to the registry
#[structopt(author="", name="publish")]
Publish(Publish),
#[structopt(author="", name="install")]
/// Install a module from the registry
#[structopt(author="", name="install")]
Install(Install),
#[structopt(author="", name="search")]
/// Search in the registry
#[structopt(author="", name="search")]
Search(Search),
#[structopt(author="", name="select")]
/// Insert into the database
#[structopt(author="", name="add")]
Add(cmd::add_cmd::Args),
/// Select from the database
#[structopt(author="", name="select")]
Select(cmd::select_cmd::Args),
#[structopt(author="", name="completions")]
/// Delete from the database
#[structopt(author="", name="delete")]
Delete(cmd::delete_cmd::Args),
/// Verify blob storage for corrupt and dangling blobs
#[structopt(author="", name="fsck")]
Fsck(cmd::fsck_cmd::Args),
/// Generate shell completions
#[structopt(author="", name="completions")]
Completions(Completions),
}
#[derive(Debug, StructOpt)]
pub struct Run {
/// Execute a module that has been installed
pub module: Option<String>,
#[structopt(short="f", long="file", conflicts_with="module")]
pub module: String,
/// Run a module from a path
pub file: Option<String>,
#[structopt(short="j", long="threads", default_value="1")]
#[structopt(short="f", long="file")]
pub file: bool,
/// Run modules concurrently
#[structopt(short="j", long="threads", default_value="1")]
pub threads: usize,
#[structopt(short="v", long="verbose", parse(from_occurrences))]
/// Verbose logging, once to print inserts even if they don't add new
/// data, twice to activate the debug() function
#[structopt(short="v", long="verbose", parse(from_occurrences))]
pub verbose: u64,
#[structopt(long="stdin")]
/// Expose stdin to modules
#[structopt(long="stdin")]
pub stdin: bool,
#[structopt(long="grant")]
/// Automatically grant access to a keyring namespace
#[structopt(long="grant")]
pub grants: Vec<String>,
#[structopt(long="grant-full-keyring")]
/// Automatically grant access to all requested keys
#[structopt(long="grant-full-keyring")]
pub grant_full_keyring: bool,
#[structopt(long="deny-keyring")]
/// Automatically deny access to all requested keys
#[structopt(long="deny-keyring")]
pub deny_keyring: bool,
#[structopt(short="x", long="exit-on-error")]
/// Exit on first error and set exit code
#[structopt(short="x", long="exit-on-error")]
pub exit_on_error: bool,
#[structopt(short="o", long="option")]
/// Set an option
#[structopt(short="o", long="option")]
pub options: Vec<options::Opt>,
}

View File

@@ -1,6 +1,7 @@
use crate::errors::*;
use opener;
use std::fs;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use crate::api::Client;
@@ -9,17 +10,18 @@ use crate::paths;
use crate::term;
pub fn load_token() -> Result<String> {
fn path() -> Result<PathBuf> {
let path = paths::data_dir()?;
let path = path.join("auth");
let session = fs::read_to_string(path)?;
Ok(path.join("auth"))
}
pub fn load_token() -> Result<String> {
let session = fs::read_to_string(path()?)?;
Ok(session.trim().to_string())
}
pub fn save_token(session: &str) -> Result<()> {
let path = paths::data_dir()?;
let path = path.join("auth");
fs::write(path, format!("{}\n", session))?;
fs::write(path()?, format!("{}\n", session))?;
Ok(())
}

231
src/blobs.rs Normal file
View File

@@ -0,0 +1,231 @@
use crate::errors::*;
use crate::paths;
use crate::worker::{EventWithCallback, Event2};
use crate::workspaces::Workspace;
use blake2::VarBlake2b;
use digest::{Input, VariableOutput};
use bytes::Bytes;
use serde::ser::{Serialize, Serializer};
use serde::de::{self, Deserialize, Deserializer};
use std::fs;
use std::path::PathBuf;
use std::result;
use std::sync::mpsc;
#[derive(Debug, Clone, PartialEq)]
pub struct Blob {
pub id: String,
pub bytes: Bytes,
}
impl Blob {
pub fn create(bytes: Bytes) -> Blob {
let id = Self::hash(&bytes);
Blob {
id,
bytes,
}
}
pub fn hash(bytes: &[u8]) -> String {
let mut h = VarBlake2b::new(32).unwrap();
h.input(bytes);
Self::encode_hash(&h.vec_result())
}
#[inline]
fn encode_hash(bytes: &[u8]) -> String {
let x = bs58::encode(bytes).into_string();
format!("{:0<44}", x)
}
}
impl EventWithCallback for Blob {
type Payload = ();
fn with_callback(self, tx: mpsc::Sender<result::Result<Self::Payload, String>>) -> Event2 {
Event2::Blob((self, tx))
}
}
impl Serialize for Blob {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = base64::encode(&self.bytes);
serializer.serialize_str(&s)
}
}
impl<'de> Deserialize<'de> for Blob {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = base64::decode(&s)
.map_err(de::Error::custom)?;
Ok(Blob::create(Bytes::from(bytes)))
}
}
pub struct BlobStorage {
path: PathBuf,
}
impl BlobStorage {
#[inline]
pub fn new<I: Into<PathBuf>>(path: I) -> BlobStorage {
BlobStorage {
path: path.into(),
}
}
pub fn workspace(workspace: &Workspace) -> Result<BlobStorage> {
let path = paths::blobs_dir(workspace)?;
Ok(BlobStorage::new(path))
}
pub fn join(&self, id: &str) -> Result<PathBuf> {
if !id.chars().all(char::is_alphanumeric) {
bail!("blob id contains invalid characters");
}
Ok(self.path.join(id))
}
pub fn load(&self, id: &str) -> Result<Blob> {
let path = self.join(id)?;
debug!("Loading blob from {:?}", path);
let bytes = fs::read(path)
.context("Failed to read blob")?;
Ok(Blob {
id: id.to_string(),
bytes: Bytes::from(bytes),
})
}
pub fn save(&self, blob: &Blob) -> Result<()> {
let path = self.join(&blob.id)?;
debug!("Writing blob to {:?}", path);
fs::write(path, &blob.bytes)
.context("Failed to write blob")?;
Ok(())
}
pub fn delete(&self, id: &str) -> Result<()> {
let path = self.join(id)?;
debug!("Deleting blob: {:?}", path);
fs::remove_file(path)
.context("Failed to delete blob")?;
Ok(())
}
pub fn list(&self) -> Result<Vec<String>> {
let mut blobs = Vec::new();
for entry in fs::read_dir(&self.path)? {
let blob = entry?
.file_name()
.into_string()
.map_err(|_| format_err!("Invalid filename"))?;
blobs.push(blob);
}
Ok(blobs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile;
use serde_json;
#[inline]
fn blob() -> (Bytes, Blob) {
let bytes = Bytes::from(&b"asdf"[..]);
(bytes.clone(), Blob::create(bytes))
}
#[test]
fn verify_create_blob() {
let (bytes, blob) = blob();
assert_eq!(blob, Blob {
id: String::from("DTTV3EjpHBNJx3Zw7eJsVPm4bYXKmNkJQpVNkcvTtTSz"),
bytes,
});
}
#[test]
fn test_blobstorage_save() {
let dir = tempfile::tempdir().expect("tempdir");
let s = BlobStorage::new(dir.path());
let (_, blob1) = blob();
s.save(&blob1).expect("save failed");
let blob2 = s.load("DTTV3EjpHBNJx3Zw7eJsVPm4bYXKmNkJQpVNkcvTtTSz").expect("load failed");
assert_eq!(blob1, blob2);
}
#[test]
fn test_blobstorage_load_failure() {
let dir = tempfile::tempdir().expect("tempdir");
let s = BlobStorage::new(dir.path());
let result = s.load("DTTV3EjpHBNJx3Zw7eJsVPm4bYXKmNkJQpVNkcvTtTSz");
assert!(result.is_err());
}
#[test]
fn test_path_validation() {
let dir = tempfile::tempdir().expect("tempdir");
let s = BlobStorage::new(dir.path());
let result = s.load("../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd");
assert!(result.is_err());
}
#[test]
fn test_serialize() {
let (_, blob) = blob();
let json = serde_json::to_string(&blob).unwrap();
assert_eq!(&json, "\"YXNkZg==\"");
}
#[test]
fn test_deserialize() {
let (_, blob1) = blob();
let blob2: Blob = serde_json::from_str("\"YXNkZg==\"").unwrap();
assert_eq!(blob1, blob2);
}
#[test]
fn test_hash_encoding() {
let x = bs58::decode("22es54J4FbFtpb5D1MtBazVuum4TcqCQ7M9JkmYdmJ8W")
.into_vec()
.unwrap();
let x = Blob::encode_hash(&x);
assert_eq!(x.len(), 44);
assert_eq!(x, "22es54J4FbFtpb5D1MtBazVuum4TcqCQ7M9JkmYdmJ8W");
}
#[test]
fn test_hash_encoding_padding() {
let x = bs58::decode("r6edvU326yvpXLubYacXXSxf2HzqCgzqHUQvpWyNwei")
.into_vec()
.unwrap();
let x = Blob::encode_hash(&x);
assert_eq!(x.len(), 44);
assert_eq!(x, "r6edvU326yvpXLubYacXXSxf2HzqCgzqHUQvpWyNwei0");
}
}

View File

@@ -1,28 +1,92 @@
use crate::errors::*;
use crate::blobs::Blob;
use crate::cmd::Cmd;
use crate::gfx;
use crate::models::*;
use crate::shell::Readline;
use structopt::StructOpt;
use structopt::clap::AppSettings;
use crate::utils;
use crate::term;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
#[derive(Debug, StructOpt)]
#[structopt(author = "",
raw(global_settings = "&[AppSettings::ColoredHelp]"))]
pub enum Args {
pub struct Args {
#[structopt(subcommand)]
subcommand: Target,
/// Do not actually insert into database
#[structopt(short="n", long="dry-run")]
dry_run: bool,
}
#[derive(Debug, StructOpt)]
pub enum Target {
/// Insert domain into the database
#[structopt(name="domain")]
Domain(AddDomain),
/// Insert subdomain into the database
#[structopt(name="subdomain")]
Subdomain(AddSubdomain),
/// Insert email into the database
#[structopt(name="email")]
Email(AddEmail),
/// Insert phonenumber into the database
#[structopt(name="phonenumber")]
PhoneNumber(AddPhoneNumber),
/// Insert device into the database
#[structopt(name="device")]
Device(AddDevice),
/// Insert network into the database
#[structopt(name="network")]
Network(AddNetwork),
/// Insert account into the database
#[structopt(name="account")]
Account(AddAccount),
/// Insert breach into the database
#[structopt(name="breach")]
Breach(AddBreach),
/// Insert images into the database
#[structopt(name="image")]
Image(AddImage),
}
impl Cmd for Args {
fn run(self, rl: &mut Readline) -> Result<()> {
match self.subcommand {
Target::Domain(args) => args.insert(rl, self.dry_run),
Target::Subdomain(args) => args.insert(rl, self.dry_run),
Target::Email(args) => args.insert(rl, self.dry_run),
Target::PhoneNumber(args) => args.insert(rl, self.dry_run),
Target::Device(args) => args.insert(rl, self.dry_run),
Target::Network(args) => args.insert(rl, self.dry_run),
Target::Account(args) => args.insert(rl, self.dry_run),
Target::Breach(args) => args.insert(rl, self.dry_run),
Target::Image(args) => args.insert(rl, self.dry_run),
}
}
}
#[inline]
pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
Args::run_str(rl, args)
}
trait IntoInsert: Sized {
fn into_insert(self, rl: &Readline) -> Result<Insert>;
fn insert(self, rl: &Readline, dry_run: bool) -> Result<()> {
let insert = self.into_insert(rl)?;
if !dry_run {
rl.db().insert_generic(insert)?;
}
Ok(())
}
}
#[derive(Debug, StructOpt)]
@@ -30,28 +94,143 @@ pub struct AddDomain {
domain: Option<String>,
}
impl IntoInsert for AddDomain {
fn into_insert(self, rl: &Readline) -> Result<Insert> {
let domain = match self.domain {
Some(domain) => domain,
_ => utils::question("Domain")?,
};
// ensure input is a valid domain
let parsed_domain = rl.psl().parse_domain(&domain)
.map_err(|e| format_err!("Failed to parse domain: {}", e))?;
if Some(domain.as_str()) != parsed_domain.root() {
bail!("This is not a valid domain, might be a subdomain or tld");
}
Ok(Insert::Domain(NewDomain {
value: domain,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddSubdomain {
subdomain: Option<String>,
}
impl IntoInsert for AddSubdomain {
fn into_insert(self, rl: &Readline) -> Result<Insert> {
let subdomain = match self.subdomain {
Some(subdomain) => subdomain,
_ => utils::question("Subdomain")?,
};
let dns_name = rl.psl().parse_dns_name(&subdomain)
.map_err(|e| format_err!("Failed to parse dns_name: {}", e))?;
let domain = dns_name.domain()
.ok_or_else(|| format_err!("Dns Name seems invalid"))?
.to_string();
let domain_id = match rl.db().insert_struct(NewDomain {
value: domain,
})? {
Some((_, domain_id)) => domain_id,
_ => bail!("Domain is out out of scope"),
};
Ok(Insert::Subdomain(NewSubdomain {
domain_id,
value: subdomain,
resolvable: None,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddEmail {
email: Option<String>,
}
impl IntoInsert for AddEmail {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
let email = match self.email {
Some(email) => email,
_ => utils::question("Email")?,
};
Ok(Insert::Email(NewEmail {
value: email,
displayname: None,
valid: None,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddPhoneNumber {
phonenumber: Option<String>,
name: Option<String>,
}
impl IntoInsert for AddPhoneNumber {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
let (phonenumber, name) = match self.phonenumber {
Some(phonenumber) => (phonenumber, self.name),
_ => {
let phonenumber = utils::question("Phone Number")?;
let name = utils::question_opt("Name")?;
(phonenumber, name)
},
};
Ok(Insert::PhoneNumber(NewPhoneNumber {
value: phonenumber,
name: name,
valid: None,
last_online: None,
country: None,
carrier: None,
line: None,
is_ported: None,
last_ported: None,
caller_name: None,
caller_type: None,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddDevice {
mac: Option<String>,
name: Option<String>,
}
impl IntoInsert for AddDevice {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
let (mac, name) = match self.mac {
Some(mac) => {
(mac, self.name)
},
_ => {
let mac = utils::question("Mac address")?;
let name = utils::question_opt("Name")?;
(mac, name)
},
};
Ok(Insert::Device(NewDevice {
value: mac,
name: name,
hostname: None,
vendor: None,
last_seen: None,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddNetwork {
network: Option<String>,
@@ -59,150 +238,177 @@ pub struct AddNetwork {
longitude: Option<f32>,
}
pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
let args = Args::from_iter_safe(args)?;
match args {
Args::Domain(args) => add_domain(rl, args),
Args::Subdomain(args) => add_subdomain(rl, args),
Args::Email(args) => add_email(rl, args),
Args::PhoneNumber(args) => add_phonenumber(rl, args),
Args::Device(args) => add_device(rl, args),
Args::Network(args) => add_network(rl, args),
impl IntoInsert for AddNetwork {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
let (network, latitude, longitude) = match self.network {
Some(network) => (network, self.latitude, self.longitude),
_ => {
let network = utils::question("Network")?;
let latitude = utils::question_typed_opt("Latitude")?;
let longitude = utils::question_typed_opt("Longitude")?;
(network, latitude, longitude)
}
};
Ok(Insert::Network(NewNetwork {
value: network,
latitude,
longitude,
}))
}
}
fn add_domain(rl: &mut Readline, args: AddDomain) -> Result<()> {
let domain = match args.domain {
Some(domain) => domain,
_ => utils::question("Domain")?,
};
// ensure input is a valid domain
let parsed_domain = rl.psl().parse_domain(&domain)
.map_err(|e| format_err!("Failed to parse domain: {}", e))?;
if Some(domain.as_str()) != parsed_domain.root() {
bail!("This is not a valid domain, might be a subdomain or tld");
}
rl.db().insert_struct(NewDomain {
value: &domain,
})?;
Ok(())
#[derive(Debug, StructOpt)]
pub struct AddAccount {
service: Option<String>,
username: Option<String>,
}
fn add_subdomain(rl: &mut Readline, args: AddSubdomain) -> Result<()> {
let subdomain = match args.subdomain {
Some(subdomain) => subdomain,
_ => utils::question("Subdomain")?,
};
impl IntoInsert for AddAccount {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
let (service, username) = match (self.service, self.username) {
(Some(service), Some(username)) => (service, username),
_ => {
let service = utils::question("Service")?;
let username = utils::question("Username")?;
let dns_name = rl.psl().parse_dns_name(&subdomain)
.map_err(|e| format_err!("Failed to parse dns_name: {}", e))?;
(service, username)
},
};
let domain = dns_name.domain()
.ok_or_else(|| format_err!("Dns Name seems invalid"))?
.to_string();
let domain_id = match rl.db().insert_struct(NewDomain {
value: &domain,
})? {
Some((_, domain_id)) => domain_id,
_ => bail!("Domain is out out of scope"),
};
rl.db().insert_struct(NewSubdomain {
domain_id,
value: &subdomain,
resolvable: None,
})?;
Ok(())
}
fn add_email(rl: &mut Readline, args: AddEmail) -> Result<()> {
let email = match args.email {
Some(email) => email,
_ => utils::question("Email")?,
};
rl.db().insert_struct(NewEmail {
value: &email,
valid: None,
})?;
Ok(())
}
fn add_phonenumber(rl: &mut Readline, args: AddPhoneNumber) -> Result<()> {
let (phonenumber, name) = match args.phonenumber {
Some(phonenumber) => {
(phonenumber, args.name)
},
_ => {
let phonenumber = utils::question("Phone Number")?;
let name = utils::question_opt("Name")?;
(phonenumber, name)
},
};
rl.db().insert_struct(NewPhoneNumber {
value: &phonenumber,
name: name.as_ref(),
valid: None,
last_online: None,
country: None,
carrier: None,
line: None,
is_ported: None,
last_ported: None,
caller_name: None,
caller_type: None,
})?;
Ok(())
}
fn add_device(rl: &mut Readline, args: AddDevice) -> Result<()> {
let (mac, name) = match args.mac {
Some(mac) => {
(mac, args.name)
},
_ => {
let mac = utils::question("Mac address")?;
let name = utils::question_opt("Name")?;
(mac, name)
},
};
rl.db().insert_struct(NewDevice {
value: &mac,
name: name.as_ref(),
hostname: None,
vendor: None,
last_seen: None,
})?;
Ok(())
}
fn add_network(rl: &mut Readline, args: AddNetwork) -> Result<()> {
let (network, latitude, longitude) = match args.network {
Some(network) => (network, args.latitude, args.longitude),
_ => {
let network = utils::question("Network")?;
let latitude = utils::question_typed_opt("Latitude")?;
let longitude = utils::question_typed_opt("Longitude")?;
(network, latitude, longitude)
if service.contains('/') {
// TODO: avoid duplication
bail!("Service field can't contain `/`");
}
};
rl.db().insert_struct(NewNetwork {
value: &network,
latitude,
longitude,
})?;
let value = format!("{}/{}", service, username);
Ok(())
Ok(Insert::Account(NewAccount {
value: value,
service: service,
username: username,
displayname: None,
email: None,
url: None,
last_seen: None,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddBreach {
name: Option<String>,
}
impl IntoInsert for AddBreach {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
let name = match self.name {
Some(name) => name,
_ => {
let name = utils::question("Name")?;
name
}
};
Ok(Insert::Breach(NewBreach {
value: name,
}))
}
}
#[derive(Debug, StructOpt)]
pub struct AddImage {
paths: Vec<String>,
}
impl IntoInsert for AddImage {
fn into_insert(self, _rl: &Readline) -> Result<Insert> {
unreachable!()
}
fn insert(self, rl: &Readline, dry_run: bool) -> Result<()> {
let paths = if self.paths.is_empty() {
let path = utils::question("Path")?;
vec![path]
} else {
self.paths
};
for path in paths {
for path in WalkDir::new(path) {
let path = match path {
Ok(path) => path,
Err(err) => {
let path = err.path().unwrap_or(Path::new("")).display();
let err = err.io_error()
.map(|err| err.to_string())
.unwrap_or_else(|| String::from("walkdir failed"));
term::error(&format!("Failed to access entry {:?}: {}", path, err));
continue;
},
};
if path.file_type().is_dir() {
debug!("Traversing into directory: {:?}", path);
continue;
}
debug!("Testing: {:?}", path.path());
let data = match fs::read(path.path()) {
Ok(data) => data,
Err(err) => {
term::error(&format!("Failed to read {:?}: {}", path, err));
continue;
},
};
// check if image
let format = match gfx::guess_format(&data) {
Ok(format) => format.mime().to_string(),
_ => {
debug!("Probably not an image, skipping");
continue;
},
};
debug!("Detected image format: {:?}", format);
let blob = Blob::create(data.into());
if !dry_run {
rl.blobs().save(&blob)?;
}
let value = blob.id;
let filename = path.file_name()
.to_string_lossy()
.to_string();
term::info(&format!("{} {:?}", value, path.path()));
if !dry_run {
rl.db().insert_generic(Insert::Image(NewImage {
value,
filename: Some(filename),
mime: Some(format),
width: None,
height: None,
created: None,
latitude: None,
longitude: None,
nudity: None,
ahash: None,
dhash: None,
phash: None,
}))?;
}
}
}
Ok(())
}
}

View File

@@ -1,6 +1,7 @@
use crate::errors::*;
use crate::db;
use crate::cmd::Cmd;
use crate::filters::{Target, Filter};
use crate::shell::Readline;
use structopt::StructOpt;
use structopt::clap::AppSettings;
@@ -11,52 +12,37 @@ use crate::term;
#[derive(Debug, StructOpt)]
#[structopt(author = "",
raw(global_settings = "&[AppSettings::ColoredHelp]"))]
pub enum Args {
#[structopt(name="domains")]
Domains(Filter),
#[structopt(name="subdomains")]
Subdomains(Filter),
#[structopt(name="ipaddrs")]
IpAddrs(Filter),
#[structopt(name="urls")]
Urls(Filter),
#[structopt(name="emails")]
Emails(Filter),
#[structopt(name="phonenumbers")]
PhoneNumbers(Filter),
#[structopt(name="devices")]
Devices(Filter),
#[structopt(name="networks")]
Networks(Filter),
pub struct Args {
#[structopt(subcommand)]
subcommand: Target,
}
#[derive(Debug, StructOpt)]
pub struct Filter {
args: Vec<String>,
}
impl Filter {
pub fn parse(&self) -> Result<db::Filter> {
db::Filter::parse(&self.args)
impl Cmd for Args {
fn run(self, rl: &mut Readline) -> Result<()> {
let rows = match &self.subcommand {
Target::Domains(filter) => delete::<Domain>(rl, &filter),
Target::Subdomains(filter) => delete::<Subdomain>(rl, &filter),
Target::IpAddrs(filter) => delete::<IpAddr>(rl, &filter),
Target::Urls(filter) => delete::<Url>(rl, &filter),
Target::Emails(filter) => delete::<Email>(rl, &filter),
Target::PhoneNumbers(filter) => delete::<PhoneNumber>(rl, &filter),
Target::Devices(filter) => delete::<Device>(rl, &filter),
Target::Networks(filter) => delete::<Network>(rl, &filter),
Target::Accounts(filter) => delete::<Account>(rl, &filter),
Target::Breaches(filter) => delete::<Breach>(rl, &filter),
Target::Images(filter) => delete::<Image>(rl, &filter),
}?;
term::info(&format!("Deleted {} rows", rows));
Ok(())
}
}
#[inline]
pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
let args = Args::from_iter_safe(args)?;
let rows = match args {
Args::Domains(filter) => delete::<Domain>(rl, &filter),
Args::Subdomains(filter) => delete::<Subdomain>(rl, &filter),
Args::IpAddrs(filter) => delete::<IpAddr>(rl, &filter),
Args::Urls(filter) => delete::<Url>(rl, &filter),
Args::Emails(filter) => delete::<Email>(rl, &filter),
Args::PhoneNumbers(filter) => delete::<PhoneNumber>(rl, &filter),
Args::Devices(filter) => delete::<Device>(rl, &filter),
Args::Networks(filter) => delete::<Network>(rl, &filter),
}?;
term::info(&format!("Deleted {} rows", rows));
Ok(())
Args::run_str(rl, args)
}
#[inline]
fn delete<T: Model + Detailed>(rl: &mut Readline, filter: &Filter) -> Result<usize> {
rl.db().delete::<T>(&filter.parse()?)
T::delete(rl.db(), &filter.parse()?)
}

88
src/cmd/fsck_cmd.rs Normal file
View File

@@ -0,0 +1,88 @@
use crate::errors::*;
use crate::blobs::Blob;
use crate::cmd::Cmd;
use crate::shell::Readline;
use crate::term;
use crate::worker;
use crate::models::*;
use std::collections::HashSet;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub struct Args {
/// Verbose output
#[structopt(short="v", long="verbose", parse(from_occurrences))]
verbose: u64,
/// Delete only dangling blobs
#[structopt(long="gc")]
gc: bool,
/// Delete dangling and corrupted blobs
#[structopt(long="gc-all")]
gc_all: bool,
}
impl Cmd for Args {
fn run(self, rl: &mut Readline) -> Result<()> {
let blobs = rl.blobs();
let hashset = worker::spawn_fn("Building reference set...", || {
let mut hashset = HashSet::new();
for x in rl.db().list::<Image>()? {
hashset.insert(x.value);
}
Ok(hashset)
}, true)?;
for blob in blobs.list()? {
let state = worker::spawn_fn(&blob, || {
let blob = rl.blobs().load(&blob)?;
// ensure content matches hash
if Blob::hash(&blob.bytes) != blob.id {
Ok(State::Corrupted)
// ensure blob is referenced by any table
} else if !hashset.contains(&blob.id) {
Ok(State::Dangling)
} else {
Ok(State::Valid)
}
}, true);
match state {
Ok(State::Valid) => {
if self.verbose > 0 {
term::info(&format!("{}... ok", blob));
}
},
Ok(State::Dangling) => {
term::warn(&format!("{}... dangling", blob));
if self.gc || self.gc_all {
blobs.delete(&blob)?;
}
},
Ok(State::Corrupted) => {
term::error(&format!("{}... corrupted", blob));
if self.gc_all {
blobs.delete(&blob)?;
}
},
Err(err) => {
term::error(&format!("{}... {}", blob, err));
},
}
}
Ok(())
}
}
enum State {
Valid,
Dangling,
Corrupted,
}

View File

@@ -2,7 +2,7 @@ use crate::errors::*;
use crate::shell::Readline;
pub trait Cmd: structopt::StructOpt + Sized {
fn run(&self, rl: &mut Readline) -> Result<()>;
fn run(self, rl: &mut Readline) -> Result<()>;
#[inline]
fn run_str(rl: &mut Readline, args: &[String]) -> Result<()> {
@@ -13,6 +13,7 @@ pub trait Cmd: structopt::StructOpt + Sized {
pub mod add_cmd;
pub mod delete_cmd;
pub mod fsck_cmd;
pub mod help_cmd;
pub mod run_cmd;
pub mod use_cmd;

View File

@@ -8,6 +8,7 @@ use colored::Colorize;
use crate::engine::Module;
use crate::registry;
use crate::shell::Readline;
use crate::update::AutoUpdater;
use structopt::StructOpt;
use structopt::clap::AppSettings;
use crate::term;
@@ -24,20 +25,20 @@ pub struct Args {
#[derive(Debug, StructOpt)]
pub enum SubCommand {
#[structopt(author="", name="list")]
/// List installed modules
#[structopt(author="", name="list")]
List(List),
#[structopt(author="", name="install")]
/// Install module from registry
#[structopt(author="", name="install")]
Install(args::Install),
#[structopt(author="", name="search")]
/// Search modules in registry
#[structopt(author="", name="search")]
Search(args::Search),
#[structopt(author="", name="reload")]
/// Reload modules
#[structopt(author="", name="reload")]
Reload(Reload),
#[structopt(author="", name="update")]
/// Update modules
#[structopt(author="", name="update")]
Update(Update),
}
@@ -54,18 +55,27 @@ pub struct Update {
}
fn update(client: &Client, config: &Config, module: &Module) -> Result<()> {
let name = module.canonical();
let installed = module.version();
let infos = client.query_module(&module.id())?;
let label = format!("Searching for updates {}", name);
let infos = worker::spawn_fn(&label, || {
client.query_module(&module.id())
}, true)?;
debug!("Latest version: {:?}", infos);
let latest = infos.latest.ok_or_else(|| format_err!("Module doesn't have any released versions"))?;
if installed != latest {
term::info(&format!("Updating {}: {:?} -> {:?}", module.canonical(), installed, latest));
registry::run_install(&Install {
module: module.id(),
version: None,
}, &config)?;
let label = format!("Updating {}: {:?} -> {:?}", &name, installed, latest);
worker::spawn_fn(&label, || {
registry::run_install(&Install {
module: module.id(),
version: None,
}, &config)
}, true)?;
term::success(&format!("Updated {}: {:?} -> {:?}", &name, installed, latest));
}
Ok(())
@@ -105,17 +115,25 @@ pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
SubCommand::Update(_) => {
let client = Client::new(&config)?;
let mut success = true;
for module in rl.engine().list() {
let name = module.canonical();
let label = format!("Searching for updates {}", name);
let result = worker::spawn_fn(&label, || {
update(&client, &config, &module)
}, true);
if let Err(err) = result {
term::error(&format!("Failed to update {}: {:?}", name, err));
if module.is_private() {
debug!("{} is a private module, skipping", module.canonical());
continue;
}
if let Err(err) = update(&client, &config, &module) {
term::error(&format!("Failed to update {}: {}", module.canonical(), err));
success = false;
}
}
// TODO: keep a list of outdated packages and remove them after they've been updated
if success {
let mut autoupdate = AutoUpdater::load()?;
autoupdate.all_updated();
autoupdate.save()?;
}
// trigger reload

View File

@@ -1,6 +1,6 @@
use crate::errors::*;
use crate::db;
use crate::filters::{Target, Filter};
use crate::shell::Readline;
use structopt::StructOpt;
use structopt::clap::AppSettings;
@@ -11,46 +11,31 @@ use crate::term;
#[derive(Debug, StructOpt)]
#[structopt(author = "",
raw(global_settings = "&[AppSettings::ColoredHelp]"))]
pub enum Args {
#[structopt(name="domains")]
Domains(Filter),
#[structopt(name="subdomains")]
Subdomains(Filter),
#[structopt(name="ipaddrs")]
IpAddrs(Filter),
#[structopt(name="urls")]
Urls(Filter),
#[structopt(name="emails")]
Emails(Filter),
#[structopt(name="phonenumbers")]
PhoneNumbers(Filter),
}
#[derive(Debug, StructOpt)]
pub struct Filter {
args: Vec<String>,
}
impl Filter {
pub fn parse(&self) -> Result<db::Filter> {
db::Filter::parse(&self.args)
}
pub struct Args {
#[structopt(subcommand)]
subcommand: Target,
}
pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
let args = Args::from_iter_safe(args)?;
let rows = match args {
Args::Domains(filter) => noscope::<Domain>(rl, &filter),
Args::Subdomains(filter) => noscope::<Subdomain>(rl, &filter),
Args::IpAddrs(filter) => noscope::<IpAddr>(rl, &filter),
Args::Urls(filter) => noscope::<Url>(rl, &filter),
Args::Emails(filter) => noscope::<Email>(rl, &filter),
Args::PhoneNumbers(filter) => noscope::<PhoneNumber>(rl, &filter),
let rows = match args.subcommand {
Target::Domains(filter) => noscope::<Domain>(rl, &filter),
Target::Subdomains(filter) => noscope::<Subdomain>(rl, &filter),
Target::IpAddrs(filter) => noscope::<IpAddr>(rl, &filter),
Target::Urls(filter) => noscope::<Url>(rl, &filter),
Target::Emails(filter) => noscope::<Email>(rl, &filter),
Target::PhoneNumbers(filter) => noscope::<PhoneNumber>(rl, &filter),
Target::Devices(filter) => noscope::<Device>(rl, &filter),
Target::Networks(filter) => noscope::<Network>(rl, &filter),
Target::Accounts(filter) => noscope::<Account>(rl, &filter),
Target::Breaches(filter) => noscope::<Breach>(rl, &filter),
Target::Images(filter) => noscope::<Image>(rl, &filter),
}?;
term::info(&format!("Updated {} rows", rows));
Ok(())
}
#[inline]
fn noscope<T: Model + Detailed>(rl: &mut Readline, filter: &Filter) -> Result<usize> {
rl.db().noscope::<T>(&filter.parse()?)
T::noscope(rl.db(), &filter.parse()?)
}

View File

@@ -1,8 +1,8 @@
use crate::errors::*;
use crate::args;
use crate::db::{Database, Filter};
use crate::db::ttl;
use crate::blobs::{Blob, BlobStorage};
use crate::db::{ttl, Filter};
use crate::engine::Module;
use crate::models::*;
use crate::shell::Readline;
@@ -67,16 +67,26 @@ impl From<Args> for Params<'static> {
}
}
fn prepare_arg<T: Serialize + Model>(x: T) -> Result<(serde_json::Value, Option<String>)> {
fn prepare_arg<T: Serialize + Model>(bs: &BlobStorage, x: T) -> Result<(serde_json::Value, Option<String>, Vec<Blob>)> {
let pretty = x.to_string();
let blobs = if let Some(blob) = x.blob() {
let blob = bs.load(blob)?;
vec![blob]
} else {
Vec::new()
};
let arg = serde_json::to_value(x)?;
Ok((arg, Some(pretty)))
Ok((arg, Some(pretty), blobs))
}
fn prepare_args<T: Scopable + Serialize + Model>(db: &Database, filter: &Filter) -> Result<Vec<(serde_json::Value, Option<String>)>> {
db.filter::<T>(filter)?
fn prepare_args<T: Scopable + Serialize + Model>(rl: &Readline, filter: &Filter, param: Option<&String>) -> Result<Vec<(serde_json::Value, Option<String>, Vec<Blob>)>> {
let db = rl.db();
let bs = rl.blobs();
db.filter_with_param::<T>(filter, param)?
.into_iter()
.map(prepare_arg)
.map(|x| prepare_arg(bs, x))
.collect()
}
@@ -113,12 +123,17 @@ pub fn execute(rl: &mut Readline, params: Params, options: HashMap<String, Strin
let filter = rl.scoped_targets();
let args = match module.source() {
Some(Source::Domains) => prepare_args::<Domain>(rl.db(), &filter),
Some(Source::Subdomains) => prepare_args::<Subdomain>(rl.db(), &filter),
Some(Source::IpAddrs) => prepare_args::<IpAddr>(rl.db(), &filter),
Some(Source::Urls) => prepare_args::<Url>(rl.db(), &filter),
Some(Source::Emails) => prepare_args::<Email>(rl.db(), &filter),
Some(Source::PhoneNumbers) => prepare_args::<PhoneNumber>(rl.db(), &filter),
Some(Source::Domains) => prepare_args::<Domain>(rl, &filter, None),
Some(Source::Subdomains) => prepare_args::<Subdomain>(rl, &filter, None),
Some(Source::IpAddrs) => prepare_args::<IpAddr>(rl, &filter, None),
Some(Source::Urls) => prepare_args::<Url>(rl, &filter, None),
Some(Source::Emails) => prepare_args::<Email>(rl, &filter, None),
Some(Source::PhoneNumbers) => prepare_args::<PhoneNumber>(rl, &filter, None),
Some(Source::Networks) => prepare_args::<Network>(rl, &filter, None),
Some(Source::Devices) => prepare_args::<Device>(rl, &filter, None),
Some(Source::Accounts(service)) => prepare_args::<Account>(rl, &filter, service.as_ref()),
Some(Source::Breaches) => prepare_args::<Breach>(rl, &filter, None),
Some(Source::Images) => prepare_args::<Image>(rl, &filter, None),
Some(Source::KeyRing(namespace)) => {
let keyring = rl.keyring();
if keyring.is_access_granted(&module, &namespace) {
@@ -126,14 +141,14 @@ pub fn execute(rl: &mut Readline, params: Params, options: HashMap<String, Strin
.map(|key| {
let pretty = format!("{}:{}", key.namespace, key.access_key);
let arg = serde_json::to_value(key)?;
Ok((arg, Some(pretty)))
Ok((arg, Some(pretty), vec![]))
})
.collect::<Result<Vec<_>>>()
} else {
Ok(vec![])
}
},
None => Ok(vec![(serde_json::Value::Null, None)]),
None => Ok(vec![(serde_json::Value::Null, None, vec![])]),
}?;
rl.signal_register().catch_ctrl();

View File

@@ -1,6 +1,6 @@
use crate::errors::*;
use crate::db;
use crate::filters::{Target, Filter};
use crate::shell::Readline;
use structopt::StructOpt;
use structopt::clap::AppSettings;
@@ -11,46 +11,31 @@ use crate::term;
#[derive(Debug, StructOpt)]
#[structopt(author = "",
raw(global_settings = "&[AppSettings::ColoredHelp]"))]
pub enum Args {
#[structopt(name="domains")]
Domains(Filter),
#[structopt(name="subdomains")]
Subdomains(Filter),
#[structopt(name="ipaddrs")]
IpAddrs(Filter),
#[structopt(name="urls")]
Urls(Filter),
#[structopt(name="emails")]
Emails(Filter),
#[structopt(name="phonenumbers")]
PhoneNumbers(Filter),
}
#[derive(Debug, StructOpt)]
pub struct Filter {
args: Vec<String>,
}
impl Filter {
pub fn parse(&self) -> Result<db::Filter> {
db::Filter::parse(&self.args)
}
pub struct Args {
#[structopt(subcommand)]
subcommand: Target,
}
pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
let args = Args::from_iter_safe(args)?;
let rows = match args {
Args::Domains(filter) => scope::<Domain>(rl, &filter),
Args::Subdomains(filter) => scope::<Subdomain>(rl, &filter),
Args::IpAddrs(filter) => scope::<IpAddr>(rl, &filter),
Args::Urls(filter) => scope::<Url>(rl, &filter),
Args::Emails(filter) => scope::<Email>(rl, &filter),
Args::PhoneNumbers(filter) => scope::<PhoneNumber>(rl, &filter),
let rows = match args.subcommand {
Target::Domains(filter) => scope::<Domain>(rl, &filter),
Target::Subdomains(filter) => scope::<Subdomain>(rl, &filter),
Target::IpAddrs(filter) => scope::<IpAddr>(rl, &filter),
Target::Urls(filter) => scope::<Url>(rl, &filter),
Target::Emails(filter) => scope::<Email>(rl, &filter),
Target::PhoneNumbers(filter) => scope::<PhoneNumber>(rl, &filter),
Target::Devices(filter) => scope::<Device>(rl, &filter),
Target::Networks(filter) => scope::<Network>(rl, &filter),
Target::Accounts(filter) => scope::<Account>(rl, &filter),
Target::Breaches(filter) => scope::<Breach>(rl, &filter),
Target::Images(filter) => scope::<Image>(rl, &filter),
}?;
term::info(&format!("Updated {} rows", rows));
Ok(())
}
#[inline]
fn scope<T: Model + Detailed>(rl: &mut Readline, filter: &Filter) -> Result<usize> {
rl.db().scope::<T>(&filter.parse()?)
T::scope(rl.db(), &filter.parse()?)
}

View File

@@ -1,8 +1,8 @@
use crate::errors::*;
use crate::cmd::Cmd;
use crate::db;
use crate::db::ttl;
use crate::filters::{Target, Filter};
use crate::shell::Readline;
use serde::Serialize;
use serde_json;
@@ -17,70 +17,61 @@ use crate::models::*;
pub struct Args {
#[structopt(subcommand)]
subcommand: Target,
#[structopt(long="json")]
/// Json output
/// Print json output
#[structopt(long="json", group="output")]
json: bool,
/// Print paths to blobs
#[structopt(long="paths", group="output")]
paths: bool,
}
#[derive(Debug, StructOpt)]
pub enum Target {
#[structopt(name="domains")]
/// Select domains
Domains(Filter),
#[structopt(name="subdomains")]
/// Select subdomains
Subdomains(Filter),
#[structopt(name="ipaddrs")]
/// Select ipaddrs
IpAddrs(Filter),
#[structopt(name="urls")]
/// Select urls
Urls(Filter),
#[structopt(name="emails")]
/// Select emails
Emails(Filter),
#[structopt(name="phonenumbers")]
/// Select phone numbers
PhoneNumbers(Filter),
#[structopt(name="devices")]
/// Select devices
Devices(Filter),
#[structopt(name="networks")]
/// Select networks
Networks(Filter),
enum Output {
Normal,
Json,
Paths,
}
#[derive(Debug, StructOpt)]
pub struct Filter {
args: Vec<String>,
struct Printer<'a, 'b> {
rl: &'a mut Readline<'b>,
output: Output,
}
impl Filter {
pub fn parse(&self) -> Result<db::Filter> {
db::Filter::parse_optional(&self.args)
}
}
impl<'a, 'b> Printer<'a, 'b> {
pub fn new(rl: &'a mut Readline<'b>, args: &Args) -> Printer<'a, 'b> {
let output = if args.json {
Output::Json
} else if args.paths {
Output::Paths
} else {
Output::Normal
};
pub struct Printer<'a> {
rl: &'a mut Readline,
json: bool,
}
impl<'a> Printer<'a> {
pub fn new(rl: &'a mut Readline, json: bool) -> Printer<'a> {
Printer {
rl,
json,
output,
}
}
pub fn select<T: Model + Detailed + Serialize>(&self, filter: &Filter) -> Result<()> {
for obj in self.rl.db().filter::<T>(&filter.parse()?)? {
if self.json {
let v = serde_json::to_string(&obj)?;
println!("{}", v);
} else {
println!("{}", obj.detailed(self.rl.db())?);
for obj in self.rl.db().filter::<T>(&filter.parse_optional()?)? {
match self.output {
Output::Normal => println!("{}", obj.detailed(self.rl.db())?),
Output::Json => {
let v = serde_json::to_string(&obj)?;
println!("{}", v);
},
Output::Paths => {
let blob = obj.blob()
.ok_or_else(|| format_err!("This model isn't linked to blob storage"))?;
let path = self.rl.blobs()
.join(blob)?;
let path = path.to_str()
.ok_or_else(|| format_err!("Path is invalid utf-8"))?;
println!("{}", path);
},
}
}
@@ -89,8 +80,8 @@ impl<'a> Printer<'a> {
}
impl Cmd for Args {
fn run(&self, rl: &mut Readline) -> Result<()> {
let printer = Printer::new(rl, self.json);
fn run(self, rl: &mut Readline) -> Result<()> {
let printer = Printer::new(rl, &self);
match &self.subcommand {
Target::Domains(filter) => printer.select::<Domain>(&filter),
@@ -101,6 +92,9 @@ impl Cmd for Args {
Target::PhoneNumbers(filter) => printer.select::<PhoneNumber>(&filter),
Target::Devices(filter) => printer.select::<Device>(&filter),
Target::Networks(filter) => printer.select::<Network>(&filter),
Target::Accounts(filter) => printer.select::<Account>(&filter),
Target::Breaches(filter) => printer.select::<Breach>(&filter),
Target::Images(filter) => printer.select::<Image>(&filter),
}
}
}

View File

@@ -31,12 +31,17 @@ pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
if args.filter.is_empty() {
match source {
Source::Domains => select::<Domain>(rl)?,
Source::Subdomains => select::<Subdomain>(rl)?,
Source::IpAddrs => select::<IpAddr>(rl)?,
Source::Urls => select::<Url>(rl)?,
Source::Emails => select::<Email>(rl)?,
Source::PhoneNumbers => select::<PhoneNumber>(rl)?,
Source::Domains => select::<Domain>(rl, None)?,
Source::Subdomains => select::<Subdomain>(rl, None)?,
Source::IpAddrs => select::<IpAddr>(rl, None)?,
Source::Urls => select::<Url>(rl, None)?,
Source::Emails => select::<Email>(rl, None)?,
Source::PhoneNumbers => select::<PhoneNumber>(rl, None)?,
Source::Networks => select::<Network>(rl, None)?,
Source::Devices => select::<Device>(rl, None)?,
Source::Accounts(service) => select::<Account>(rl, service.as_ref())?,
Source::Breaches => select::<Breach>(rl, None)?,
Source::Images => select::<Image>(rl, None)?,
Source::KeyRing(namespace) => {
for key in rl.keyring().list_for(&namespace) {
println!("{}:{}", key.namespace, key.name);
@@ -64,15 +69,20 @@ fn count_selected(rl: &mut Readline, source: &Source) -> Result<usize> {
Source::Urls => db.filter::<Url>(&filter)?.len(),
Source::Emails => db.filter::<Email>(&filter)?.len(),
Source::PhoneNumbers => db.filter::<PhoneNumber>(&filter)?.len(),
Source::Networks => db.filter::<Network>(&filter)?.len(),
Source::Devices => db.filter::<Device>(&filter)?.len(),
Source::Accounts(service) => db.filter_with_param::<Account>(&filter, service.as_ref())?.len(),
Source::Breaches => db.filter::<Breach>(&filter)?.len(),
Source::Images => db.filter::<Image>(&filter)?.len(),
Source::KeyRing(namespace) => rl.keyring().list_for(&namespace).len(),
};
Ok(num)
}
fn select<T: Model + Detailed>(rl: &mut Readline) -> Result<()> {
fn select<T: Model + Detailed>(rl: &mut Readline, param: Option<&String>) -> Result<()> {
let filter = rl.scoped_targets();
for obj in rl.db().filter::<T>(&filter)? {
for obj in rl.db().filter_with_param::<T>(&filter, param)? {
println!("{}", obj.detailed(rl.db())?);
}

View File

@@ -1,5 +1,6 @@
use crate::errors::*;
use crate::blobs::BlobStorage;
use crate::db::Database;
use crate::shell::Readline;
use structopt::StructOpt;
@@ -18,7 +19,9 @@ pub fn run(rl: &mut Readline, args: &[String]) -> Result<()> {
let args = Args::from_iter_safe(args)?;
if let Some(workspace) = args.workspace {
let blobs = BlobStorage::workspace(&workspace)?;
let db = Database::establish(workspace)?;
rl.set_blobstorage(blobs);
rl.set_db(db);
} else {
for x in workspaces::list()? {

View File

@@ -1,6 +1,6 @@
use crate::args::{Args, Completions};
use crate::errors::*;
use rustyline;
use rustyline::{self, Context};
use rustyline::completion::Completer;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
@@ -33,7 +33,10 @@ impl CmdCompleter {
"emails",
"phonenumbers",
"devices",
"networks"];
"networks",
"accounts",
"breaches",
"images"];
let results: Vec<String> = options.iter()
.filter(|x| x.starts_with(arg))
@@ -47,7 +50,7 @@ impl CmdCompleter {
impl Completer for CmdCompleter {
type Candidate = String;
fn complete(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec<String>)> {
fn complete(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> rustyline::Result<(usize, Vec<String>)> {
if line.len() != pos {
return Ok((0, vec![]));
}
@@ -93,7 +96,10 @@ impl Completer for CmdCompleter {
"email",
"phonenumber",
"device",
"network"];
"network",
"account",
"breach",
"image"];
let results: Vec<String> = options.iter()
.filter(|x| x.starts_with(arg))
@@ -187,9 +193,9 @@ impl Completer for CmdCompleter {
// TODO: suggest rest of the line if only one possible completion
impl Hinter for CmdCompleter {
#[inline]
fn hint(&self, _line: &str, _pos: usize) -> Option<String> {
fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<String> {
// None
match self.complete(_line, _pos) {
match self.complete(_line, _pos, _ctx) {
Ok((_, mut cmds)) => if cmds.len() == 1 {
// TODO: this fails if we complete a 2nd argument
let hint = cmds.remove(0);

View File

@@ -1,5 +1,6 @@
use dirs;
use crate::errors::*;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::net::SocketAddr;
@@ -11,6 +12,8 @@ pub struct Config {
#[serde(default)]
pub core: CoreConfig,
#[serde(default)]
pub namespaces: HashMap<String, PathBuf>,
#[serde(default)]
pub network: NetworkConfig,
}
@@ -50,12 +53,15 @@ impl Config {
pub struct CoreConfig {
#[serde(default="default_registry")]
pub registry: String,
#[serde(default, rename="no-autoupdate")]
pub no_autoupdate: bool,
}
impl Default for CoreConfig {
fn default() -> CoreConfig {
CoreConfig {
registry: default_registry(),
no_autoupdate: false,
}
}
}

View File

@@ -44,6 +44,10 @@ pub enum Family {
Device,
Network,
NetworkDevice,
Account,
Breach,
BreachEmail,
Image,
}
impl FromStr for Family {
@@ -61,6 +65,10 @@ impl FromStr for Family {
"device" => Family::Device,
"network" => Family::Network,
"network-device" => Family::NetworkDevice,
"account" => Family::Account,
"breach" => Family::Breach,
"breach-email" => Family::BreachEmail,
"image" => Family::Image,
_ => bail!("Unknown object family"),
})
}
@@ -109,80 +117,22 @@ impl Database {
}
/// Returns true if we didn't have this value yet
pub fn insert_generic(&self, object: &Insert) -> Result<Option<(DbChange, i32)>> {
pub fn insert_generic(&self, object: Insert) -> Result<Option<(DbChange, i32)>> {
match object {
Insert::Domain(object) => self.insert_struct(NewDomain {
value: &object.value,
}),
Insert::Subdomain(object) => self.insert_struct(NewSubdomain {
domain_id: object.domain_id,
value: &object.value,
resolvable: object.resolvable,
}),
Insert::IpAddr(object) => self.insert_struct(NewIpAddr {
family: &object.family,
value: &object.value,
continent: object.continent.as_ref(),
continent_code: object.continent_code.as_ref(),
country: object.country.as_ref(),
country_code: object.country_code.as_ref(),
city: object.city.as_ref(),
longitude: object.longitude,
latitude: object.latitude,
asn: object.asn,
as_org: object.as_org.as_ref(),
description: object.description.as_ref(),
reverse_dns: object.reverse_dns.as_ref(),
}),
Insert::SubdomainIpAddr(object) => self.insert_subdomain_ipaddr_struct(&NewSubdomainIpAddr {
subdomain_id: object.subdomain_id,
ip_addr_id: object.ip_addr_id,
}),
Insert::Url(object) => self.insert_struct(NewUrl {
subdomain_id: object.subdomain_id,
value: &object.value,
path: &object.path,
status: object.status,
body: object.body.as_ref(),
online: object.online,
title: object.title.as_ref(),
redirect: object.redirect.as_ref(),
}),
Insert::Email(object) => self.insert_struct(NewEmail {
value: &object.value,
valid: object.valid,
}),
Insert::PhoneNumber(object) => self.insert_struct(NewPhoneNumber {
value: &object.value,
name: object.name.as_ref(),
valid: object.valid,
last_online: object.last_online,
country: object.country.as_ref(),
carrier: object.carrier.as_ref(),
line: object.line.as_ref(),
is_ported: object.is_ported,
last_ported: object.last_ported,
caller_name: object.caller_name.as_ref(),
caller_type: object.caller_type.as_ref(),
}),
Insert::Device(object) => self.insert_struct(NewDevice {
value: &object.value,
name: object.name.as_ref(),
hostname: object.hostname.as_ref(),
vendor: object.vendor.as_ref(),
last_seen: object.last_seen,
}),
Insert::Network(object) => self.insert_struct(NewNetwork {
value: &object.value,
latitude: object.latitude,
longitude: object.longitude,
}),
Insert::NetworkDevice(object) => self.insert_network_device_struct(&NewNetworkDevice {
network_id: object.network_id,
device_id: object.device_id,
ipaddr: object.ipaddr.as_ref(),
last_seen: object.last_seen,
}),
Insert::Domain(object) => self.insert_struct(object),
Insert::Subdomain(object) => self.insert_struct(object),
Insert::IpAddr(object) => self.insert_struct(object),
Insert::SubdomainIpAddr(object) => self.insert_subdomain_ipaddr_struct(&object),
Insert::Url(object) => self.insert_struct(object),
Insert::Email(object) => self.insert_struct(object),
Insert::PhoneNumber(object) => self.insert_struct(object),
Insert::Device(object) => self.insert_struct(object),
Insert::Network(object) => self.insert_struct(object),
Insert::NetworkDevice(object) => self.insert_network_device_struct(&object),
Insert::Account(object) => self.insert_struct(object),
Insert::Breach(object) => self.insert_struct(object),
Insert::BreachEmail(object) => self.insert_breach_email_struct(object),
Insert::Image(object) => self.insert_struct(object),
}
}
@@ -221,8 +171,8 @@ impl Database {
}
pub fn insert_network_device_struct(&self, network_device: &NewNetworkDevice) -> Result<Option<(DbChange, i32)>> {
if let Some(subdomain_ipaddr_id) = NetworkDevice::get_id_opt(self, &(network_device.network_id, network_device.device_id))? {
Ok(Some((DbChange::None, subdomain_ipaddr_id)))
if let Some(network_device_id) = NetworkDevice::get_id_opt(self, &(network_device.network_id, network_device.device_id))? {
Ok(Some((DbChange::None, network_device_id)))
} else {
diesel::insert_into(network_devices::table)
.values(network_device)
@@ -232,6 +182,28 @@ impl Database {
}
}
pub fn insert_breach_email_struct(&self, obj: NewBreachEmail) -> Result<Option<(DbChange, i32)>> {
let value = &(obj.breach_id, obj.email_id, obj.password.clone());
if let Some(existing) = BreachEmail::get_opt(self, value)? {
let id = <BreachEmail as Model>::id(&existing);
let update = obj.upsert(&existing);
if update.is_dirty() {
update.apply(&self)?;
Ok(Some((DbChange::Update(update.generic()), id)))
} else {
Ok(Some((DbChange::None, id)))
}
} else {
diesel::insert_into(breach_emails::table)
.values(obj)
.execute(&self.db)?;
let id = BreachEmail::get_id(self, value)?;
Ok(Some((DbChange::Insert, id)))
}
}
//
pub fn update_generic(&self, object: &Update) -> Result<i32> {
@@ -244,6 +216,9 @@ impl Database {
Update::Device(object) => self.update_device(object),
Update::Network(object) => self.update_network(object),
Update::NetworkDevice(object) => self.update_network_device(object),
Update::Account(object) => self.update_account(object),
Update::BreachEmail(object) => self.update_breach_email(object),
Update::Image(object) => self.update_image(object),
}
}
@@ -311,6 +286,30 @@ impl Database {
Ok(network_device.id)
}
pub fn update_account(&self, account: &AccountUpdate) -> Result<i32> {
use crate::schema::accounts::columns::*;
diesel::update(accounts::table.filter(id.eq(account.id)))
.set(account)
.execute(&self.db)?;
Ok(account.id)
}
pub fn update_breach_email(&self, breach_email: &BreachEmailUpdate) -> Result<i32> {
use crate::schema::breach_emails::columns::*;
diesel::update(breach_emails::table.filter(id.eq(breach_email.id)))
.set(breach_email)
.execute(&self.db)?;
Ok(breach_email.id)
}
pub fn update_image(&self, image: &ImageUpdate) -> Result<i32> {
use crate::schema::images::columns::*;
diesel::update(images::table.filter(id.eq(image.id)))
.set(image)
.execute(&self.db)?;
Ok(image.id)
}
fn get_opt_typed<T: Model + Scopable>(&self, value: &T::ID) -> Result<Option<i32>> {
match T::get_opt(self, &value)? {
Some(ref obj) if obj.scoped() => Ok(Some(obj.id())),
@@ -330,6 +329,10 @@ impl Database {
Family::Device => self.get_opt_typed::<Device>(&value),
Family::Network => self.get_opt_typed::<Network>(&value),
Family::NetworkDevice => bail!("Unsupported operation"),
Family::Account => self.get_opt_typed::<Account>(&value),
Family::Breach => self.get_opt_typed::<Breach>(&value),
Family::BreachEmail => bail!("Unsupported operation"),
Family::Image => self.get_opt_typed::<Image>(&value),
}
}
@@ -343,16 +346,11 @@ impl Database {
T::filter(self, filter)
}
pub fn scope<T: Scopable>(&self, filter: &Filter) -> Result<usize> {
T::scope(self, filter)
}
pub fn noscope<T: Scopable>(&self, filter: &Filter) -> Result<usize> {
T::noscope(self, filter)
}
pub fn delete<T: Model>(&self, filter: &Filter) -> Result<usize> {
T::delete(self, filter)
pub fn filter_with_param<T: Model>(&self, filter: &Filter, param: Option<&String>) -> Result<Vec<T>> {
match param {
Some(param) => T::filter_with_param(self, filter, param),
_ => T::filter(self, filter),
}
}
}
@@ -398,6 +396,12 @@ impl Filter {
let mut expect_value = false;
for arg in args {
if ["=", "!=", "<", ">", "<=", ">=", "like"].contains(&arg.to_lowercase().as_str()) {
expect_value = true;
query += &format!(" {}", arg);
continue;
}
if let Some(idx) = arg.find('=') {
if idx != 0 {
let (key, value) = arg.split_at(idx);
@@ -411,10 +415,6 @@ impl Filter {
query.push_str(&Self::escape(arg));
expect_value = false;
} else {
if ["=", "!=", "like"].contains(&arg.to_lowercase().as_str()) {
expect_value = true;
}
query += &format!(" {}", arg);
}
}
@@ -538,4 +538,44 @@ mod tests {
]).unwrap();
assert_eq!(filter, Filter::new(" value = 'a''b'"));
}
#[test]
fn test_filter_greater() {
let filter = Filter::parse(&["where".to_string(),
"value".to_string(),
">".to_string(),
"123".to_string(),
]).unwrap();
assert_eq!(filter, Filter::new(" value > '123'"));
}
#[test]
fn test_filter_smaller() {
let filter = Filter::parse(&["where".to_string(),
"value".to_string(),
"<".to_string(),
"123".to_string(),
]).unwrap();
assert_eq!(filter, Filter::new(" value < '123'"));
}
#[test]
fn test_filter_greater_equal() {
let filter = Filter::parse(&["where".to_string(),
"value".to_string(),
">=".to_string(),
"123".to_string(),
]).unwrap();
assert_eq!(filter, Filter::new(" value >= '123'"));
}
#[test]
fn test_filter_smaller_equal() {
let filter = Filter::parse(&["where".to_string(),
"value".to_string(),
"<=".to_string(),
"123".to_string(),
]).unwrap();
assert_eq!(filter, Filter::new(" value <= '123'"));
}
}

View File

@@ -1,5 +1,6 @@
use crate::errors::*;
use crate::blobs::Blob;
use crate::db::Family;
use crate::engine::{Environment, Reporter};
use crate::geoip::{GeoIP, AsnDB};
@@ -120,6 +121,18 @@ pub trait State {
fn http_request(&self, session_id: &str, method: String, url: String, options: RequestOptions) -> HttpRequest;
fn register_in_jar(&self, session: &str, key: String, value: String);
fn register_blob(&self, blob: Blob) -> String;
fn get_blob(&self, id: &str) -> Result<Arc<Blob>>;
fn persist_blob(&self, id: &str) -> Result<()> {
let blob = self.get_blob(id)?;
self.send(&Event::Blob(blob.as_ref().clone()));
let reply = self.recv()?;
let reply: result::Result<(), String> = serde_json::from_value(reply)?;
reply.map_err(|err| format_err!("Failed to store blob: {:?}", err))
}
}
#[derive(Debug)]
@@ -127,8 +140,10 @@ pub struct LuaState {
error: Mutex<Option<Error>>,
logger: Arc<Mutex<Box<Reporter>>>,
socket_sessions: Mutex<HashMap<String, Arc<Mutex<Socket>>>>,
blobs: Mutex<HashMap<String, Arc<Blob>>>,
http_sessions: Mutex<HashMap<String, HttpSession>>,
http: chrootable_https::Client<Resolver>,
verbose: u64,
keyring: Vec<KeyRingEntry>, // TODO: maybe hashmap
dns_config: Resolver,
@@ -171,9 +186,9 @@ impl State for LuaState {
self.verbose
}
fn keyring(&self, namespace: &str) -> Vec<&KeyRingEntry> {
fn keyring(&self, query: &str) -> Vec<&KeyRingEntry> {
self.keyring.iter()
.filter(|x| x.namespace == namespace)
.filter(|x| x.matches(query))
.collect()
}
@@ -245,6 +260,23 @@ impl State for LuaState {
session.cookies.register_in_jar(key, value);
}
}
fn register_blob(&self, blob: Blob) -> String {
let id = blob.id.clone();
let mut mtx = self.blobs.lock().unwrap();
mtx.insert(id.clone(), Arc::new(blob));
debug!("Registered blob: {:?}", id);
id
}
fn get_blob(&self, id: &str) -> Result<Arc<Blob>> {
let mtx = self.blobs.lock().unwrap();
let blob = mtx.get(id)
.ok_or_else(|| format_err!("Invalid blob reference"))?;
Ok(blob.clone())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -269,6 +301,7 @@ fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<Reporter>>>) -> (hlua::Lua<'a
error: Mutex::new(None),
logger,
socket_sessions: Mutex::new(HashMap::new()),
blobs: Mutex::new(HashMap::new()),
http_sessions: Mutex::new(HashMap::new()),
http,
@@ -282,7 +315,13 @@ fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<Reporter>>>) -> (hlua::Lua<'a
options: env.options,
});
debug!("Adding all blobs from StartCommand");
for blob in env.blobs {
state.register_blob(blob);
}
runtime::clear_err(&mut lua, state.clone());
runtime::create_blob(&mut lua, state.clone());
runtime::datetime(&mut lua, state.clone());
runtime::db_add(&mut lua, state.clone());
runtime::db_add_ttl(&mut lua, state.clone());
@@ -300,19 +339,28 @@ fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<Reporter>>>) -> (hlua::Lua<'a
runtime::http_mksession(&mut lua, state.clone());
runtime::http_request(&mut lua, state.clone());
runtime::http_send(&mut lua, state.clone());
runtime::img_exif(&mut lua, state.clone());
runtime::img_load(&mut lua, state.clone());
runtime::img_nudity(&mut lua, state.clone());
runtime::info(&mut lua, state.clone());
runtime::json_decode(&mut lua, state.clone());
runtime::json_decode_stream(&mut lua, state.clone());
runtime::json_encode(&mut lua, state.clone());
runtime::keyring(&mut lua, state.clone());
runtime::last_err(&mut lua, state.clone());
runtime::md5(&mut lua, state.clone());
runtime::pgp_pubkey(&mut lua, state.clone());
runtime::pgp_pubkey_armored(&mut lua, state.clone());
runtime::print(&mut lua, state.clone());
runtime::psl_domain_from_dns_name(&mut lua, state.clone());
runtime::regex_find(&mut lua, state.clone());
runtime::regex_find_all(&mut lua, state.clone());
runtime::semver_match(&mut lua, state.clone());
runtime::sha1(&mut lua, state.clone());
runtime::sha2_256(&mut lua, state.clone());
runtime::sha2_512(&mut lua, state.clone());
runtime::sleep(&mut lua, state.clone());
runtime::sn0int_version(&mut lua, state.clone());
runtime::sock_connect(&mut lua, state.clone());
runtime::sock_send(&mut lua, state.clone());
runtime::sock_recv(&mut lua, state.clone());
@@ -412,6 +460,7 @@ com
dns_config,
proxy,
options: HashMap::new(),
blobs: Vec::new(),
psl,
geoip,
asn,

View File

@@ -1,5 +1,6 @@
use crate::errors::*;
use chrootable_https::dns::Resolver;
use crate::blobs::Blob;
use crate::engine::{Environment, Module, Reporter};
use crate::geoip::{GeoIP, AsnDB, Maxmind};
use crate::keyring::KeyRingEntry;
@@ -9,6 +10,7 @@ use crate::worker::{Event, Event2, LogEvent, ExitEvent, EventSender, EventWithCa
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::io::prelude::*;
use std::io::{self, BufReader, BufRead, stdin, Stdin, Stdout};
use std::net::SocketAddr;
@@ -25,6 +27,7 @@ pub struct StartCommand {
options: HashMap<String, String>,
module: Module,
arg: serde_json::Value,
blobs: Vec<Blob>,
}
impl StartCommand {
@@ -35,6 +38,7 @@ impl StartCommand {
options: HashMap<String, String>,
module: Module,
arg: serde_json::Value,
blobs: Vec<Blob>,
) -> StartCommand {
StartCommand {
verbose,
@@ -44,6 +48,7 @@ impl StartCommand {
options,
module,
arg,
blobs,
}
}
}
@@ -56,8 +61,10 @@ pub struct Supervisor {
impl Supervisor {
pub fn setup(module: &Module) -> Result<Supervisor> {
let exe = env::current_exe()
.context("Failed to find current executable")?;
let exe = match env::current_exe() {
Ok(exe) => exe.into_os_string(),
_ => OsString::from("sn0int"),
};
let mut child = Command::new(exe)
.arg("sandbox")
@@ -181,6 +188,7 @@ pub fn spawn_module(module: Module,
has_stdin: bool,
proxy: Option<SocketAddr>,
options: HashMap<String, String>,
blobs: Vec<Blob>,
) -> Result<ExitEvent> {
let dns_config = Resolver::from_system()?;
@@ -191,13 +199,14 @@ pub fn spawn_module(module: Module,
};
let mut supervisor = Supervisor::setup(&module)?;
supervisor.send_start(&StartCommand::new(verbose, keyring, dns_config, proxy, options, module, arg))?;
supervisor.send_start(&StartCommand::new(verbose, keyring, dns_config, proxy, options, module, arg, blobs))?;
let exit = loop {
match supervisor.recv()? {
Event::Log(event) => tx.send(Event2::Log(event)),
Event::Database(object) => supervisor.send_event_callback(object, &tx),
Event::Stdio(object) => object.apply(&mut supervisor, tx, &mut reader),
Event::Blob(blob) => supervisor.send_event_callback(blob, &tx),
Event::Exit(event) => {
if let ExitEvent::Err(err) = &event {
tx.send(Event2::Log(LogEvent::Error(err.clone())));
@@ -227,6 +236,7 @@ pub fn run_worker(geoip: Vec<u8>, asn: Vec<u8>, psl: &str) -> Result<()> {
dns_config: start.dns_config,
proxy: start.proxy,
options: start.options,
blobs: start.blobs,
psl,
geoip,
asn,

View File

@@ -1,5 +1,7 @@
use crate::errors::*;
use crate::blobs::Blob;
use crate::config::Config;
use crate::geoip::{GeoIP, AsnDB};
use crate::json::LuaJsonValue;
use crate::keyring::KeyRingEntry;
@@ -17,6 +19,7 @@ use chrootable_https::dns::Resolver;
use crate::psl::Psl;
use crate::paths;
use std::cmp::Ordering;
use std::path::Path;
use crate::term;
use crate::worker::{self, Event};
@@ -33,24 +36,27 @@ pub struct Environment {
pub dns_config: Resolver,
pub proxy: Option<SocketAddr>,
pub options: HashMap<String, String>,
pub blobs: Vec<Blob>,
pub psl: Psl,
pub geoip: GeoIP,
pub asn: AsnDB,
}
#[derive(Debug)]
pub struct Engine {
pub struct Engine<'a> {
path: PathBuf,
modules: HashMap<String, Vec<Module>>,
config: &'a Config
}
impl Engine {
pub fn new(verbose_init: bool) -> Result<Engine> {
impl<'a> Engine<'a> {
pub fn new(verbose_init: bool, config: &'a Config) -> Result<Engine> {
let path = paths::module_dir()?;
let mut engine = Engine {
path,
modules: HashMap::new(),
config,
};
if verbose_init {
@@ -72,53 +78,103 @@ impl Engine {
Ok(modules)
}
pub fn private_modules(path: &Path) -> Result<bool> {
let metadata = fs::symlink_metadata(&path)?.file_type();
if metadata.is_symlink() {
debug!("Folder is a symlink, flagging modules as private");
return Ok(true);
}
if path.join(".git").exists() {
debug!("Folder is a git repo, flagging modules as private");
return Ok(true);
}
Ok(false)
}
pub fn reload_modules_quiet(&mut self) -> Result<()> {
self.modules = HashMap::new();
for author in fs::read_dir(&self.path)? {
let author = author?;
let path = author.path();
if !author.path().is_dir() {
if !path.is_dir() {
continue;
}
let private_modules = Self::private_modules(&path)?;
let author_name = author.file_name()
.into_string()
.map_err(|_| format_err!("Failed to decode filename"))?;
for module in fs::read_dir(&author.path())? {
let module = module?;
let module_name = module.file_name()
.into_string()
.map_err(|_| format_err!("Failed to decode filename"))?;
// find last instance of .lua in filename, if any
let (module_name, ext) = if let Some(idx) = module_name.rfind(".lua") {
module_name.split_at(idx)
} else {
// TODO: show warning
continue;
};
self.load_module_folder(&path, &author_name, private_modules)?;
}
// if .lua is not at the end, skip
if ext != ".lua" {
// TODO: show warning
continue;
}
for (author, folder) in &self.config.namespaces {
let folder = if folder.is_absolute() {
folder.to_owned()
} else {
let folder = folder.strip_prefix("~/")
.unwrap_or(&folder);
let path = module.path();
let module_name = module_name.to_string();
let module = Module::load(&path, &author_name, &module_name)
.context(format!("Failed to parse {}/{}", author_name, module_name))?;
dirs::home_dir()
.ok_or_else(|| format_err!("Failed to find home folder"))?
.join(folder)
};
for key in &[&module_name, &format!("{}/{}", author_name, module_name)] {
if !self.modules.contains_key(*key) {
self.modules.insert(key.to_string(), Vec::new());
}
self.load_module_folder(&folder, &author, true)?;
}
let vec = self.modules.get_mut(*key).unwrap();
vec.push(module.clone());
}
Ok(())
}
pub fn load_module_folder(&mut self, folder: &Path, author_name: &str, private_modules: bool) -> Result<()> {
debug!("Loading modules from {:?}", folder);
for module in fs::read_dir(folder)? {
let module = module?;
let module_name = module.file_name()
.into_string()
.map_err(|_| format_err!("Failed to decode filename"))?;
// find last instance of .lua in filename, if any
let (module_name, ext) = if let Some(idx) = module_name.rfind(".lua") {
module_name.split_at(idx)
} else {
// TODO: show warning
continue;
};
// if .lua is not at the end, skip
if ext != ".lua" {
// TODO: show warning
continue;
}
if let Err(err) = self.load_single_module(&module.path(), &author_name, &module_name, private_modules) {
let root = err.find_root_cause();
term::warn(&format!("Failed to load {}/{}: {}", author_name, module_name, root));
}
}
Ok(())
}
pub fn load_single_module(&mut self, path: &Path, author_name: &str, module_name: &str, private_module: bool) -> Result<()> {
let module_name = module_name.to_string();
let module = Module::load(path, &author_name, &module_name, private_module)
.context(format!("Failed to parse {}/{}", author_name, module_name))?;
for key in &[&module_name, &format!("{}/{}", author_name, module_name)] {
if !self.modules.contains_key(*key) {
self.modules.insert(key.to_string(), Vec::new());
}
let vec = self.modules.get_mut(*key).unwrap();
vec.push(module.clone());
}
Ok(())
@@ -161,11 +217,12 @@ pub struct Module {
version: String,
source: Option<Source>,
keyring_access: Vec<String>,
private_module: bool,
script: Script,
}
impl Module {
pub fn load(path: &PathBuf, author: &str, name: &str) -> Result<Module> {
pub fn load(path: &Path, author: &str, name: &str, private_module: bool) -> Result<Module> {
debug!("Loading lua module {}/{} from {:?}", author, name, path);
let code = fs::read_to_string(path)
.context("Failed to read module")?;
@@ -182,10 +239,12 @@ impl Module {
version: metadata.version,
source: metadata.source,
keyring_access: metadata.keyring_access,
private_module,
script,
})
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
@@ -194,6 +253,7 @@ impl Module {
format!("{}/{}", self.author, self.name)
}
#[inline]
pub fn id(&self) -> ModuleID {
ModuleID {
author: self.author.to_string(),
@@ -201,27 +261,37 @@ impl Module {
}
}
#[inline]
pub fn description(&self) -> &str {
&self.description
}
#[inline]
pub fn version(&self) -> &str {
&self.version
}
#[inline]
pub fn source(&self) -> &Option<Source> {
&self.source
}
#[inline]
pub fn keyring_access(&self) -> &[String] {
&self.keyring_access
}
#[inline]
pub fn is_private(&self) -> bool {
self.private_module
}
pub fn run(&self, env: Environment, reporter: Arc<Mutex<Box<Reporter>>>, arg: LuaJsonValue) -> Result<()> {
debug!("Executing lua script {}", self.canonical());
self.script.run(env, reporter, arg.into())
}
#[inline]
fn cmp_canonical(&self, other: &Module) -> Ordering {
if self.author == other.author {
self.name.cmp(&other.name)

57
src/filters.rs Normal file
View File

@@ -0,0 +1,57 @@
use crate::errors::*;
use crate::db;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub enum Target {
/// On domains
#[structopt(name="domains")]
Domains(Filter),
/// On subdomains
#[structopt(name="subdomains")]
Subdomains(Filter),
/// On ipaddrs
#[structopt(name="ipaddrs")]
IpAddrs(Filter),
/// On urls
#[structopt(name="urls")]
Urls(Filter),
/// On emails
#[structopt(name="emails")]
Emails(Filter),
/// On phone numbers
#[structopt(name="phonenumbers")]
PhoneNumbers(Filter),
/// On devices
#[structopt(name="devices")]
Devices(Filter),
/// On networks
#[structopt(name="networks")]
Networks(Filter),
/// On accounts
#[structopt(name="accounts")]
Accounts(Filter),
/// On breaches
#[structopt(name="breaches")]
Breaches(Filter),
/// On images
#[structopt(name="images")]
Images(Filter),
}
#[derive(Debug, StructOpt)]
pub struct Filter {
args: Vec<String>,
}
impl Filter {
pub fn parse_optional(&self) -> Result<db::Filter> {
db::Filter::parse_optional(&self.args)
}
pub fn parse(&self) -> Result<db::Filter> {
db::Filter::parse(&self.args)
}
}

View File

@@ -27,13 +27,20 @@ pub trait Maxmind: Sized {
// TODO: refactor this to return Path
fn cache_path() -> Result<String> {
// use system path if exists
let path = Path::new("/usr/share/GeoIP/");
let path = path.join(Self::archive_filename());
for path in &[
// Archlinux
"/usr/share/GeoIP/",
// OpenBSD
"/usr/local/share/examples/libmaxminddb/",
] {
let path = Path::new(path);
let path = path.join(Self::archive_filename());
if path.exists() {
let path = path.to_str()
.ok_or_else(|| format_err!("Failed to decode path"))?;
return Ok(path.to_string());
if path.exists() {
let path = path.to_str()
.ok_or_else(|| format_err!("Failed to decode path"))?;
return Ok(path.to_string());
}
}
// use cache path
@@ -77,7 +84,8 @@ pub trait Maxmind: Sized {
debug!("Downloading {:?}...", url);
let client = Client::with_system_resolver()?;
let resp = client.get(url)
.wait_for_response()?;
.wait_for_response()
.context("http request failed")?;
debug!("Downloaded {} bytes", resp.body.len());
archive::extract(&mut &resp.body[..], filter, path)?;
Ok(())

180
src/gfx/exif.rs Normal file
View File

@@ -0,0 +1,180 @@
use crate::errors::*;
use exif;
use std::io;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Location {
latitude: f64,
longitude: f64,
}
impl Location {
pub fn try_from(fields: &[exif::Field]) -> Result<Location> {
let mut builder = LocationBuilder::default();
fields.iter()
.map(|f| builder.add_one(f))
.collect::<Result<()>>()?;
builder.build()
}
}
#[derive(Debug, Default)]
struct LocationBuilder {
latitude: Option<f64>,
latitude_ref: Option<f64>,
longitude: Option<f64>,
longitude_ref: Option<f64>,
}
impl LocationBuilder {
fn add_one(&mut self, f: &exif::Field) -> Result<()> {
debug!("Exif tag: {:?}, {}", f.tag, f.value.display_as(f.tag));
match f.tag {
exif::Tag::GPSLatitudeRef => self.latitude_ref = Some(cardinal_direction_modifier(&f.value)?),
exif::Tag::GPSLongitudeRef => self.longitude_ref = Some(cardinal_direction_modifier(&f.value)?),
exif::Tag::GPSLatitude => self.latitude = Some(dms_to_float(&f.value)?),
exif::Tag::GPSLongitude => self.longitude = Some(dms_to_float(&f.value)?),
_ => (),
};
Ok(())
}
fn build(self) -> Result<Location> {
let latitude = self.latitude
.ok_or_else(|| format_err!("Missing latitude field"))?;
let latitude_ref = self.latitude_ref
.ok_or_else(|| format_err!("Missing latitude field"))?;
let longitude = self.longitude
.ok_or_else(|| format_err!("Missing latitude field"))?;
let longitude_ref = self.longitude_ref
.ok_or_else(|| format_err!("Missing latitude field"))?;
Ok(Location {
latitude: latitude * latitude_ref,
longitude: longitude * longitude_ref,
})
}
}
pub fn gps(img: &[u8]) -> Result<Option<Location>> {
let mut buf = io::BufReader::new(img);
let reader = exif::Reader::new(&mut buf)?;
let fields = reader.fields();
debug!("Exif fields: {:?}", fields);
let location = Location::try_from(fields).ok();
Ok(location)
}
pub fn dms_to_float(dms: &exif::Value) -> Result<f64> {
let dms = match dms {
exif::Value::Rational(dms) => dms,
_ => panic!("Unexpected exif value for dms"),
};
if dms.len() != 3 {
bail!("Incorrect numbers for floats for dms");
}
let degrees = dms[0].to_f64();
let minutes = dms[1].to_f64();
let seconds = dms[2].to_f64();
let float = degrees + minutes/60.0 + seconds/3600.0;
let float = (float * 1000000.0).round() / 1000000.0;
Ok(float)
}
pub fn cardinal_direction_modifier(value: &exif::Value) -> Result<f64> {
match value {
exif::Value::Ascii(s) => {
let s = s.get(0)
.ok_or_else(|| format_err!("Cardinal direction value is empty"))?;
match s.get(0) {
Some(b'N') => Ok(1.0),
Some(b'S') => Ok(-1.0),
Some(b'E') => Ok(1.0),
Some(b'W') => Ok(-1.0),
_ => bail!("Unexpected cardinal direction"),
}
},
_ => bail!("Unexpected exif value"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_init;
#[test]
fn verify_exif_location() {
test_init();
let location = Location::try_from(&[
exif::Field {
tag: exif::Tag::GPSLatitudeRef,
thumbnail: false,
value: exif::Value::Ascii(vec![&[b'N']]),
}, exif::Field {
tag: exif::Tag::GPSLongitudeRef,
thumbnail: false,
value: exif::Value::Ascii(vec![&[b'E']]),
}, exif::Field {
tag: exif::Tag::GPSLatitude,
thumbnail: false,
value: exif::Value::Rational(vec![exif::Rational {
num: 43,
denom: 1,
}, exif::Rational {
num: 28,
denom: 1,
}, exif::Rational {
num: 176399999,
denom: 100000000,
}]),
}, exif::Field {
tag: exif::Tag::GPSLongitude,
thumbnail: false,
value: exif::Value::Rational(vec![exif::Rational {
num: 11,
denom: 1,
}, exif::Rational {
num: 53,
denom: 1,
}, exif::Rational {
num: 742199999,
denom: 100000000,
}]),
},
]).unwrap();
println!("{:?}", location);
assert_eq!(location, Location {
latitude: 43.467157,
longitude: 11.885395
});
}
#[test]
fn verify_dms() {
test_init();
let latitude = dms_to_float(&exif::Value::Rational(vec![exif::Rational {
num: 43,
denom: 1,
}, exif::Rational {
num: 28,
denom: 1,
}, exif::Rational {
num: 176399999,
denom: 100000000,
}])).unwrap();
assert_eq!(latitude, 43.467157);
}
}

124
src/gfx/mod.rs Normal file
View File

@@ -0,0 +1,124 @@
use crate::errors::*;
use image::{self, DynamicImage, GenericImageView};
pub mod exif;
#[derive(Debug)]
pub enum ImageFormat {
PNG,
JPEG,
GIF,
WEBP,
TIFF,
BMP,
ICO,
}
impl ImageFormat {
pub fn mime(&self) -> &str {
// https://www.iana.org/assignments/media-types/media-types.xhtml#image
// /etc/nginx/mime.types
match self {
ImageFormat::PNG => "image/png",
ImageFormat::JPEG => "image/jpeg",
ImageFormat::GIF => "image/gif",
ImageFormat::WEBP => "image/webp",
ImageFormat::TIFF => "image/tiff",
ImageFormat::BMP => "image/bmp",
ImageFormat::ICO => "image/vnd.microsoft.icon",
}
}
pub fn try_from(format: &image::ImageFormat) -> Result<ImageFormat> {
use image::ImageFormat::*;
match format {
PNG => Ok(ImageFormat::PNG),
JPEG => Ok(ImageFormat::JPEG),
GIF => Ok(ImageFormat::GIF),
WEBP => Ok(ImageFormat::WEBP),
TIFF => Ok(ImageFormat::TIFF),
BMP => Ok(ImageFormat::BMP),
ICO => Ok(ImageFormat::ICO),
HDR | PNM | TGA => bail!("Unsupported format: {:?}", format),
}
}
}
impl Into<image::ImageFormat> for ImageFormat {
fn into(self) -> image::ImageFormat {
match self {
ImageFormat::PNG => image::ImageFormat::PNG,
ImageFormat::JPEG => image::ImageFormat::JPEG,
ImageFormat::GIF => image::ImageFormat::GIF,
ImageFormat::WEBP => image::ImageFormat::WEBP,
ImageFormat::TIFF => image::ImageFormat::TIFF,
ImageFormat::BMP => image::ImageFormat::BMP,
ImageFormat::ICO => image::ImageFormat::ICO,
}
}
}
pub struct Image {
image: DynamicImage,
format: ImageFormat,
}
impl Image {
#[inline]
pub fn mime(&self) -> &str {
self.format.mime()
}
#[inline]
pub fn width(&self) -> u32 {
self.image.width()
}
#[inline]
pub fn height(&self) -> u32 {
self.image.height()
}
}
impl AsRef<DynamicImage> for Image {
fn as_ref(&self) -> &DynamicImage {
&self.image
}
}
#[inline]
pub fn guess_format(buf: &[u8]) -> Result<ImageFormat> {
let format = image::guess_format(buf)?;
ImageFormat::try_from(&format)
}
pub fn load(buf: &[u8]) -> Result<Image> {
let img_format = image::guess_format(&buf)?;
let format = ImageFormat::try_from(&img_format)?;
let image = image::load_from_memory_with_format(&buf, img_format)?;
Ok(Image {
image,
format,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn verify_gfx_load_ico() {
let ico = fs::read("./sn0int-registry/assets/favicon.ico").expect("fs::read");
let img = load(&ico).expect("gfx::load");
assert_eq!("image/vnd.microsoft.icon", img.mime());
assert_eq!(16, img.height());
assert_eq!(16, img.width());
}
}

View File

@@ -7,7 +7,7 @@ use crate::paths;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::str::FromStr;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use sn0int_common::ModuleID;
@@ -85,7 +85,7 @@ impl KeyRing {
}
}
pub fn load(path: &PathBuf) -> Result<KeyRing> {
pub fn load(path: &Path) -> Result<KeyRing> {
let buf = fs::read(&path)?;
serde_json::from_slice(&buf)
.map_err(Error::from)
@@ -202,6 +202,17 @@ impl KeyRingEntry {
let v = LuaJsonValue::from(v).into();
Ok(v)
}
pub fn matches(&self, query: &str) -> bool {
if let Some(idx) = query.find(':') {
let (namespace, access_key) = query.split_at(idx);
let access_key = &access_key[1..];
self.namespace == namespace && self.access_key == access_key
} else {
self.namespace == query
}
}
}
#[cfg(test)]

View File

@@ -3,8 +3,6 @@
#[macro_use] extern crate failure;
#[macro_use] extern crate maplit;
use url;
#[cfg(target_os = "openbsd")]
#[macro_use] extern crate pledge;
use hlua_badtouch as hlua;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate log;
@@ -18,6 +16,7 @@ pub mod api;
pub mod archive;
pub mod args;
pub mod auth;
pub mod blobs;
pub mod cmd;
pub mod complete;
pub mod config;
@@ -25,8 +24,10 @@ pub mod crt;
pub mod db;
pub mod errors;
pub mod engine;
pub mod filters;
pub mod fmt;
pub mod geoip;
pub mod gfx;
pub mod html;
pub mod json;
pub mod keyring;
@@ -43,7 +44,15 @@ pub mod ser;
pub mod shell;
pub mod sockets;
pub mod term;
pub mod update;
pub mod utils;
pub mod web;
pub mod worker;
pub mod workspaces;
#[cfg(test)]
fn test_init() {
use env_logger;
let _ = env_logger::builder().is_test(true).try_init();
}

View File

@@ -1,8 +1,8 @@
#![warn(unused_extern_crates)]
use env_logger;
#[macro_use] extern crate failure;
#[macro_use] extern crate log;
use env_logger::Env;
use env_logger::{self, Env};
use sn0int::args::{self, Args, SubCommand};
use sn0int::auth;
use sn0int::cmd;
@@ -22,27 +22,25 @@ use std::io::Write;
use std::path::Path;
fn run_run(gargs: &Args, args: &args::Run, config: Config) -> Result<()> {
fn run_run(gargs: &Args, args: &args::Run, config: &Config) -> Result<()> {
let mut rl = shell::init(gargs, config, false)?;
if let Some(module) = &args.module {
let module = rl.engine().get(&module)?.clone();
rl.set_module(module);
} else if let Some(file) = &args.file {
let path = Path::new(file);
let module = if args.file {
let path = Path::new(&args.module);
let filename = path.file_stem()
.ok_or(format_err!("Failed to decode filename"))?
.to_str()
.ok_or(format_err!("Failed to decode filename"))?;
let module = Module::load(&path.to_path_buf(), "anonymous", &filename)
.context(format!("Failed to parse {:?}", file))?;
rl.set_module(module);
Module::load(&path.to_path_buf(), "anonymous", &filename, true)
.context(format!("Failed to parse {:?}", path))?
} else {
bail!("At least one module or file need to be provided");
}
rl.engine().get(&args.module)?
.clone()
};
rl.set_module(module);
cmd::run_cmd::execute(&mut rl, args.into(), Opt::collect(&args.options))
}
@@ -56,7 +54,7 @@ fn run_sandbox() -> Result<()> {
engine::isolation::run_worker(geoip, asn, &psl)
}
fn run_cmd<T: cmd::Cmd>(gargs: &Args, args: &T, config: Config) -> Result<()> {
fn run_cmd<T: cmd::Cmd>(gargs: &Args, args: T, config: &Config) -> Result<()> {
let mut rl = shell::init(gargs, config, false)?;
args.run(&mut rl)
}
@@ -81,7 +79,7 @@ end
}
fn run() -> Result<()> {
let args = Args::from_args();
let mut args = Args::from_args();
if !args.is_sandbox() {
sandbox::fasten_seatbelt()?;
@@ -90,17 +88,22 @@ fn run() -> Result<()> {
let config = Config::load_or_default()
.context("Failed to load config")?;
match args.subcommand {
Some(SubCommand::Run(ref run)) => run_run(&args, run, config),
debug!("Loaded config: {:?}", config);
match args.subcommand.take() {
Some(SubCommand::Run(run)) => run_run(&args, &run, &config),
Some(SubCommand::Sandbox(_)) => run_sandbox(),
Some(SubCommand::Login(_)) => auth::run_login(&config),
Some(SubCommand::New(ref new)) => run_new(&args, new),
Some(SubCommand::Publish(ref publish)) => registry::run_publish(&args, publish, &config),
Some(SubCommand::Install(ref install)) => registry::run_install(install, &config),
Some(SubCommand::Search(ref search)) => registry::run_search(search, &config),
Some(SubCommand::Select(ref select)) => run_cmd(&args, select, config),
Some(SubCommand::Completions(ref completions)) => complete::run_generate(completions),
None => shell::run(&args, config),
Some(SubCommand::New(new)) => run_new(&args, &new),
Some(SubCommand::Publish(publish)) => registry::run_publish(&args, &publish, &config),
Some(SubCommand::Install(install)) => registry::run_install(&install, &config),
Some(SubCommand::Search(search)) => registry::run_search(&search, &config),
Some(SubCommand::Add(add)) => run_cmd(&args, add, &config),
Some(SubCommand::Select(select)) => run_cmd(&args, select, &config),
Some(SubCommand::Delete(delete)) => run_cmd(&args, delete, &config),
Some(SubCommand::Fsck(fsck)) => run_cmd(&args, fsck, &config),
Some(SubCommand::Completions(completions)) => complete::run_generate(&completions),
None => shell::run(&args, &config),
}
}

328
src/models/account.rs Normal file
View File

@@ -0,0 +1,328 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use chrono::NaiveDateTime;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="accounts"]
pub struct Account {
pub id: i32,
pub value: String,
pub service: String,
pub username: String,
pub displayname: Option<String>,
pub email: Option<String>,
pub url: Option<String>,
pub last_seen: Option<NaiveDateTime>,
pub unscoped: bool,
}
impl Model for Account {
type ID = str;
fn to_string(&self) -> String {
self.value.to_owned()
}
fn list(db: &Database) -> Result<Vec<Self>> {
use crate::schema::accounts::dsl::*;
let results = accounts.load::<Self>(db.db())?;
Ok(results)
}
fn filter(db: &Database, filter: &Filter) -> Result<Vec<Self>> {
use crate::schema::accounts::dsl::*;
let query = accounts.filter(filter.sql());
let results = query.load::<Self>(db.db())?;
Ok(results)
}
fn filter_with_param(db: &Database, filter: &Filter, param: &str) -> Result<Vec<Self>> {
use crate::schema::accounts::dsl::*;
let query = accounts
.filter(service.eq(param))
.filter(filter.sql());
let results = query.load::<Self>(db.db())?;
Ok(results)
}
fn delete(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::accounts::dsl::*;
diesel::delete(accounts.filter(filter.sql()))
.execute(db.db())
.map_err(Error::from)
}
fn delete_id(db: &Database, my_id: i32) -> Result<usize> {
use crate::schema::accounts::dsl::*;
diesel::delete(accounts.filter(id.eq(my_id)))
.execute(db.db())
.map_err(Error::from)
}
fn id(&self) -> i32 {
self.id
}
fn value(&self) -> &Self::ID {
&self.value
}
fn by_id(db: &Database, my_id: i32) -> Result<Self> {
use crate::schema::accounts::dsl::*;
let domain = accounts.filter(id.eq(my_id))
.first::<Self>(db.db())?;
Ok(domain)
}
fn get(db: &Database, query: &Self::ID) -> Result<Self> {
use crate::schema::accounts::dsl::*;
let domain = accounts.filter(value.eq(query))
.first::<Self>(db.db())?;
Ok(domain)
}
fn get_opt(db: &Database, query: &Self::ID) -> Result<Option<Self>> {
use crate::schema::accounts::dsl::*;
let domain = accounts.filter(value.eq(query))
.first::<Self>(db.db())
.optional()?;
Ok(domain)
}
}
impl Scopable for Account {
fn scoped(&self) -> bool {
!self.unscoped
}
fn scope(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::accounts::dsl::*;
diesel::update(accounts.filter(filter.sql()))
.set(unscoped.eq(false))
.execute(db.db())
.map_err(Error::from)
}
fn noscope(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::accounts::dsl::*;
diesel::update(accounts.filter(filter.sql()))
.set(unscoped.eq(true))
.execute(db.db())
.map_err(Error::from)
}
}
pub struct PrintableAccount {
value: String,
}
impl fmt::Display for PrintableAccount {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{:?}", self.value)
}
}
impl Printable<PrintableAccount> for Account {
fn printable(&self, _db: &Database) -> Result<PrintableAccount> {
Ok(PrintableAccount {
value: self.value.to_string(),
})
}
}
pub struct DetailedAccount {
id: i32,
value: String,
displayname: Option<String>,
email: Option<String>,
url: Option<String>,
last_seen: Option<NaiveDateTime>,
unscoped: bool,
}
impl DisplayableDetailed for DetailedAccount {
#[inline]
fn scoped(&self) -> bool {
!self.unscoped
}
#[inline]
fn print(&self, w: &mut fmt::DetailFormatter) -> fmt::Result {
w.id(self.id)?;
w.debug::<Green, _>(&self.value)?;
w.start_group();
w.opt_debug::<Yellow, _>(&self.displayname)?;
w.opt_debug::<Yellow, _>(&self.email)?;
w.opt_debug::<Yellow, _>(&self.url)?;
w.opt_debug::<Yellow, _>(&self.last_seen)?;
w.end_group()?;
Ok(())
}
#[inline]
fn children(&self, _w: &mut fmt::DetailFormatter) -> fmt::Result {
Ok(())
}
}
display_detailed!(DetailedAccount);
impl Detailed for Account {
type T = DetailedAccount;
fn detailed(&self, _db: &Database) -> Result<Self::T> {
Ok(DetailedAccount {
id: self.id,
value: self.value.to_string(),
displayname: self.displayname.clone(),
email: self.email.clone(),
url: self.url.clone(),
last_seen: self.last_seen.clone(),
unscoped: self.unscoped,
})
}
}
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
#[table_name="accounts"]
pub struct NewAccount {
pub value: String,
pub service: String,
pub username: String,
pub displayname: Option<String>,
pub email: Option<String>,
pub url: Option<String>,
pub last_seen: Option<NaiveDateTime>,
}
impl InsertableStruct<Account> for NewAccount {
fn value(&self) -> &str {
&self.value
}
fn insert(&self, db: &Database) -> Result<()> {
diesel::insert_into(accounts::table)
.values(self)
.execute(db.db())?;
Ok(())
}
}
impl Upsertable<Account> for NewAccount {
type Update = AccountUpdate;
fn upsert(self, existing: &Account) -> Self::Update {
Self::Update {
id: existing.id,
displayname: Self::upsert_opt(self.displayname, &existing.displayname),
email: Self::upsert_opt(self.email, &existing.email),
url: Self::upsert_opt(self.url, &existing.url),
last_seen: Self::upsert_opt(self.last_seen, &existing.last_seen),
}
}
}
impl Printable<PrintableAccount> for NewAccount {
fn printable(&self, _db: &Database) -> Result<PrintableAccount> {
Ok(PrintableAccount {
value: self.value.to_string(),
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InsertAccount {
pub service: String,
pub username: String,
pub displayname: Option<String>,
pub email: Option<String>,
pub url: Option<String>,
pub last_seen: Option<NaiveDateTime>,
}
impl LuaInsertToNew for InsertAccount {
type Target = NewAccount;
fn try_into_new(self, _state: &Arc<State>) -> Result<NewAccount> {
if self.service.contains('/') {
bail!("Service field can't contain `/`");
}
let value = format!("{}/{}", self.service, self.username);
Ok(NewAccount {
value,
service: self.service,
username: self.username,
displayname: self.displayname,
email: self.email,
url: self.url,
last_seen: self.last_seen,
})
}
}
#[derive(Identifiable, AsChangeset, Serialize, Deserialize, Debug)]
#[table_name="accounts"]
pub struct AccountUpdate {
pub id: i32,
pub displayname: Option<String>,
pub email: Option<String>,
pub url: Option<String>,
pub last_seen: Option<NaiveDateTime>,
}
impl Upsert for AccountUpdate {
fn is_dirty(&self) -> bool {
self.displayname.is_some() ||
self.email.is_some() ||
self.url.is_some() ||
self.last_seen.is_some()
}
fn generic(self) -> Update {
Update::Account(self)
}
fn apply(&self, db: &Database) -> Result<i32> {
db.update_account(&self)
}
}
impl Updateable<Account> for AccountUpdate {
fn changeset(&mut self, existing: &Account) {
Self::clear_if_equal(&mut self.displayname, &existing.displayname);
Self::clear_if_equal(&mut self.email, &existing.email);
Self::clear_if_equal(&mut self.url, &existing.url);
Self::clear_if_equal(&mut self.last_seen, &existing.last_seen);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "displayname", &self.displayname);
Self::push_value(updates, "email", &self.email);
Self::push_value(updates, "url", &self.url);
Self::push_value(updates, "last_seen", &self.last_seen);
}
}

269
src/models/breach.rs Normal file
View File

@@ -0,0 +1,269 @@
use crate::errors::*;
//use crate::fmt::Write;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="breaches"]
pub struct Breach {
pub id: i32,
pub value: String,
pub unscoped: bool,
}
impl Model for Breach {
type ID = str;
fn to_string(&self) -> String {
self.value.to_owned()
}
fn list(db: &Database) -> Result<Vec<Self>> {
use crate::schema::breaches::dsl::*;
let results = breaches.load::<Self>(db.db())?;
Ok(results)
}
fn filter(db: &Database, filter: &Filter) -> Result<Vec<Self>> {
use crate::schema::breaches::dsl::*;
let query = breaches.filter(filter.sql());
let results = query.load::<Self>(db.db())?;
Ok(results)
}
fn delete(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::breaches::dsl::*;
diesel::delete(breaches.filter(filter.sql()))
.execute(db.db())
.map_err(Error::from)
}
fn delete_id(db: &Database, my_id: i32) -> Result<usize> {
use crate::schema::breaches::dsl::*;
diesel::delete(breaches.filter(id.eq(my_id)))
.execute(db.db())
.map_err(Error::from)
}
fn id(&self) -> i32 {
self.id
}
fn value(&self) -> &Self::ID {
&self.value
}
fn by_id(db: &Database, my_id: i32) -> Result<Self> {
use crate::schema::breaches::dsl::*;
let domain = breaches.filter(id.eq(my_id))
.first::<Self>(db.db())?;
Ok(domain)
}
fn get(db: &Database, query: &Self::ID) -> Result<Self> {
use crate::schema::breaches::dsl::*;
let breach = breaches.filter(value.eq(query))
.first::<Self>(db.db())?;
Ok(breach)
}
fn get_opt(db: &Database, query: &Self::ID) -> Result<Option<Self>> {
use crate::schema::breaches::dsl::*;
let breach = breaches.filter(value.eq(query))
.first::<Self>(db.db())
.optional()?;
Ok(breach)
}
}
impl Scopable for Breach {
fn scoped(&self) -> bool {
!self.unscoped
}
fn scope(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::breaches::dsl::*;
diesel::update(breaches.filter(filter.sql()))
.set(unscoped.eq(false))
.execute(db.db())
.map_err(Error::from)
}
fn noscope(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::breaches::dsl::*;
diesel::update(breaches.filter(filter.sql()))
.set(unscoped.eq(true))
.execute(db.db())
.map_err(Error::from)
}
}
impl Breach {
fn emails(&self, db: &Database) -> Result<Vec<(Email, Option<String>)>> {
use std::result;
let email_id_pws = BreachEmail::belonging_to(self)
.select((breach_emails::email_id, breach_emails::password))
.load::<(i32, Option<String>)>(db.db())?;
email_id_pws.into_iter()
.map(|(email_id, password)| {
emails::table
.filter(emails::id.eq(email_id))
.first::<Email>(db.db())
.map(|email| (email, password))
})
.collect::<result::Result<Vec<_>, _>>()
.map_err(Error::from)
}
}
pub struct PrintableBreach {
value: String,
}
impl fmt::Display for PrintableBreach {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{:?}", self.value)
}
}
impl Printable<PrintableBreach> for Breach {
fn printable(&self, _db: &Database) -> Result<PrintableBreach> {
Ok(PrintableBreach {
value: self.value.to_string(),
})
}
}
pub struct EmailWithPassword {
email: PrintableEmail,
password: Option<String>,
}
impl fmt::Display for EmailWithPassword {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{}", self.email)?;
if let Some(password) = &self.password {
write!(w, " ({:?})", password)?;
}
Ok(())
}
}
pub struct DetailedBreach {
id: i32,
value: String,
emails: Vec<EmailWithPassword>,
unscoped: bool,
}
impl DisplayableDetailed for DetailedBreach {
#[inline]
fn scoped(&self) -> bool {
!self.unscoped
}
#[inline]
fn print(&self, w: &mut fmt::DetailFormatter) -> fmt::Result {
w.id(self.id)?;
w.debug::<Green, _>(&self.value)?;
Ok(())
}
#[inline]
fn children(&self, w: &mut fmt::DetailFormatter) -> fmt::Result {
for email in &self.emails {
w.child(email)?;
}
Ok(())
}
}
display_detailed!(DetailedBreach);
impl Detailed for Breach {
type T = DetailedBreach;
fn detailed(&self, db: &Database) -> Result<Self::T> {
let emails = self.emails(db)?.into_iter()
.map(|(sd, password)| Ok(EmailWithPassword {
email: sd.printable(db)?,
password,
}))
.collect::<Result<_>>()?;
Ok(DetailedBreach {
id: self.id,
value: self.value.to_string(),
emails,
unscoped: self.unscoped,
})
}
}
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
#[table_name="breaches"]
pub struct NewBreach {
pub value: String,
}
impl InsertableStruct<Breach> for NewBreach {
fn value(&self) -> &str {
&self.value
}
fn insert(&self, db: &Database) -> Result<()> {
diesel::insert_into(breaches::table)
.values(self)
.execute(db.db())?;
Ok(())
}
}
impl Upsertable<Breach> for NewBreach {
type Update = NullUpdate;
fn upsert(self, existing: &Breach) -> Self::Update {
Self::Update {
id: existing.id,
}
}
}
impl Printable<PrintableBreach> for NewBreach {
fn printable(&self, _db: &Database) -> Result<PrintableBreach> {
Ok(PrintableBreach {
value: self.value.to_string(),
})
}
}
pub type InsertBreach = NewBreach;
impl LuaInsertToNew for InsertBreach {
type Target = NewBreach;
fn try_into_new(self, _state: &Arc<State>) -> Result<NewBreach> {
Ok(self)
}
}

211
src/models/breach_email.rs Normal file
View File

@@ -0,0 +1,211 @@
use crate::errors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize)]
#[belongs_to(Breach)]
#[belongs_to(Email)]
#[table_name="breach_emails"]
pub struct BreachEmail {
pub id: i32,
pub breach_id: i32,
pub email_id: i32,
pub password: Option<String>,
}
impl Model for BreachEmail {
type ID = (i32, i32, Option<String>);
fn to_string(&self) -> String {
unimplemented!("BreachEmail can not be printed")
}
fn list(db: &Database) -> Result<Vec<Self>> {
use crate::schema::breach_emails::dsl::*;
let results = breach_emails.load::<Self>(db.db())?;
Ok(results)
}
fn filter(db: &Database, filter: &Filter) -> Result<Vec<Self>> {
use crate::schema::breach_emails::dsl::*;
let query = breach_emails.filter(filter.sql());
let results = query.load::<Self>(db.db())?;
Ok(results)
}
fn delete(db: &Database, filter: &Filter) -> Result<usize> {
use crate::schema::breach_emails::dsl::*;
diesel::delete(breach_emails.filter(filter.sql()))
.execute(db.db())
.map_err(Error::from)
}
fn delete_id(db: &Database, my_id: i32) -> Result<usize> {
use crate::schema::breach_emails::dsl::*;
diesel::delete(breach_emails.filter(id.eq(my_id)))
.execute(db.db())
.map_err(Error::from)
}
fn id(&self) -> i32 {
self.id
}
fn by_id(db: &Database, my_id: i32) -> Result<Self> {
use crate::schema::breach_emails::dsl::*;
let breach_email = breach_emails.filter(id.eq(my_id))
.first::<Self>(db.db())?;
Ok(breach_email)
}
fn get(db: &Database, query: &Self::ID) -> Result<Self> {
use crate::schema::breach_emails::dsl::*;
let (my_breach_id, my_email_id, my_password) = query;
let query = breach_emails.filter(breach_id.eq(my_breach_id))
.filter(email_id.eq(my_email_id));
let breach_email = if let Some(my_password) = my_password {
query
.filter(password.is_null().or(password.eq(my_password)))
.first::<Self>(db.db())?
} else {
query
.first::<Self>(db.db())?
};
Ok(breach_email)
}
fn get_opt(db: &Database, query: &Self::ID) -> Result<Option<Self>> {
use crate::schema::breach_emails::dsl::*;
let (my_breach_id, my_email_id, my_password) = query;
let query = breach_emails.filter(breach_id.eq(my_breach_id))
.filter(email_id.eq(my_email_id));
let breach_email = if let Some(my_password) = my_password {
query
.filter(password.is_null().or(password.eq(my_password)))
.first::<Self>(db.db())
.optional()?
} else {
query
.first::<Self>(db.db())
.optional()?
};
Ok(breach_email)
}
}
impl BreachEmail {
pub fn breach(&self, db: &Database) -> Result<Breach> {
Breach::by_id(db, self.breach_id)
}
}
pub struct PrintableBreachEmail {
breach: String,
email: String,
}
impl fmt::Display for PrintableBreachEmail {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{:?} -> {:?}", self.breach, self.email)
}
}
impl Printable<PrintableBreachEmail> for BreachEmail {
fn printable(&self, db: &Database) -> Result<PrintableBreachEmail> {
let breach = Breach::by_id(db, self.breach_id)?;
let email = Email::by_id(db, self.email_id)?;
Ok(PrintableBreachEmail {
breach: breach.value.to_string(),
email: email.value.to_string(),
})
}
}
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
#[table_name="breach_emails"]
pub struct NewBreachEmail {
pub breach_id: i32,
pub email_id: i32,
pub password: Option<String>,
}
impl Upsertable<BreachEmail> for NewBreachEmail {
type Update = BreachEmailUpdate;
fn upsert(self, existing: &BreachEmail) -> Self::Update {
Self::Update {
id: existing.id,
password: Self::upsert_opt(self.password, &existing.password),
}
}
}
impl Printable<PrintableBreachEmail> for NewBreachEmail {
fn printable(&self, db: &Database) -> Result<PrintableBreachEmail> {
let breach = Breach::by_id(db, self.breach_id)?;
let email = Email::by_id(db, self.email_id)?;
Ok(PrintableBreachEmail {
breach: breach.value.to_string(),
email: email.value.to_string(),
})
}
}
pub type InsertBreachEmail = NewBreachEmail;
impl LuaInsertToNew for InsertBreachEmail {
type Target = NewBreachEmail;
fn try_into_new(self, _state: &Arc<State>) -> Result<NewBreachEmail> {
Ok(self)
}
}
#[derive(Identifiable, AsChangeset, Serialize, Deserialize, Debug)]
#[table_name="breach_emails"]
pub struct BreachEmailUpdate {
pub id: i32,
pub password: Option<String>,
}
impl Upsert for BreachEmailUpdate {
fn is_dirty(&self) -> bool {
self.password.is_some()
}
fn generic(self) -> Update {
Update::BreachEmail(self)
}
fn apply(&self, db: &Database) -> Result<i32> {
db.update_breach_email(self)
}
}
impl Updateable<BreachEmail> for BreachEmailUpdate {
fn changeset(&mut self, existing: &BreachEmail) {
Self::clear_if_equal(&mut self.password, &existing.password);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "password", &self.password);
}
}

View File

@@ -4,6 +4,8 @@ use diesel;
use diesel::prelude::*;
use crate::models::*;
use chrono::NaiveDateTime;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
@@ -221,19 +223,19 @@ impl Detailed for Device {
}
}
#[derive(Insertable)]
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
#[table_name="devices"]
pub struct NewDevice<'a> {
pub value: &'a str,
pub name: Option<&'a String>,
pub hostname: Option<&'a String>,
pub vendor: Option<&'a String>,
pub struct NewDevice {
pub value: String,
pub name: Option<String>,
pub hostname: Option<String>,
pub vendor: Option<String>,
pub last_seen: Option<NaiveDateTime>,
}
impl<'a> InsertableStruct<Device> for NewDevice<'a> {
impl InsertableStruct<Device> for NewDevice {
fn value(&self) -> &str {
self.value
&self.value
}
fn insert(&self, db: &Database) -> Result<()> {
@@ -244,31 +246,21 @@ impl<'a> InsertableStruct<Device> for NewDevice<'a> {
}
}
impl<'a> Upsertable<Device> for NewDevice<'a> {
impl Upsertable<Device> for NewDevice {
type Update = DeviceUpdate;
fn upsert(self, existing: &Device) -> Self::Update {
Self::Update {
id: existing.id,
name: Self::upsert_str(self.name, &existing.name),
hostname: Self::upsert_str(self.hostname, &existing.hostname),
vendor: Self::upsert_str(self.vendor, &existing.vendor),
name: Self::upsert_opt(self.name, &existing.name),
hostname: Self::upsert_opt(self.hostname, &existing.hostname),
vendor: Self::upsert_opt(self.vendor, &existing.vendor),
last_seen: Self::upsert_opt(self.last_seen, &existing.last_seen),
}
}
}
#[derive(Debug, Insertable, Serialize, Deserialize)]
#[table_name="devices"]
pub struct NewDeviceOwned {
pub value: String,
pub name: Option<String>,
pub hostname: Option<String>,
pub vendor: Option<String>,
pub last_seen: Option<NaiveDateTime>,
}
impl Printable<PrintableDevice> for NewDeviceOwned {
impl Printable<PrintableDevice> for NewDevice {
fn printable(&self, _db: &Database) -> Result<PrintableDevice> {
Ok(PrintableDevice {
value: self.value.to_string(),
@@ -276,12 +268,12 @@ impl Printable<PrintableDevice> for NewDeviceOwned {
}
}
pub type InsertDevice = NewDeviceOwned;
pub type InsertDevice = NewDevice;
impl LuaInsertToNewOwned for InsertDevice {
type Target = NewDeviceOwned;
impl LuaInsertToNew for InsertDevice {
type Target = NewDevice;
fn try_into_new(self) -> Result<NewDeviceOwned> {
fn try_into_new(self, _state: &Arc<State>) -> Result<NewDevice> {
Ok(self)
}
}

View File

@@ -3,6 +3,8 @@ use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
@@ -188,15 +190,15 @@ impl Detailed for Domain {
}
}
#[derive(Insertable)]
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
#[table_name="domains"]
pub struct NewDomain<'a> {
pub value: &'a str,
pub struct NewDomain {
pub value: String,
}
impl<'a> InsertableStruct<Domain> for NewDomain<'a> {
impl InsertableStruct<Domain> for NewDomain {
fn value(&self) -> &str {
self.value
&self.value
}
fn insert(&self, db: &Database) -> Result<()> {
@@ -207,7 +209,7 @@ impl<'a> InsertableStruct<Domain> for NewDomain<'a> {
}
}
impl<'a> Upsertable<Domain> for NewDomain<'a> {
impl Upsertable<Domain> for NewDomain {
type Update = NullUpdate;
fn upsert(self, existing: &Domain) -> Self::Update {
@@ -217,13 +219,7 @@ impl<'a> Upsertable<Domain> for NewDomain<'a> {
}
}
#[derive(Debug, Insertable, Serialize, Deserialize)]
#[table_name="domains"]
pub struct NewDomainOwned {
pub value: String,
}
impl Printable<PrintableDomain> for NewDomainOwned {
impl Printable<PrintableDomain> for NewDomain {
fn printable(&self, _db: &Database) -> Result<PrintableDomain> {
Ok(PrintableDomain {
value: self.value.to_string(),
@@ -231,12 +227,12 @@ impl Printable<PrintableDomain> for NewDomainOwned {
}
}
pub type InsertDomain = NewDomainOwned;
pub type InsertDomain = NewDomain;
impl LuaInsertToNewOwned for InsertDomain {
type Target = NewDomainOwned;
impl LuaInsertToNew for InsertDomain {
type Target = NewDomain;
fn try_into_new(self) -> Result<NewDomainOwned> {
fn try_into_new(self, _state: &Arc<State>) -> Result<NewDomain> {
Ok(self)
}
}

View File

@@ -1,9 +1,10 @@
use crate::errors::*;
use crate::fmt::Write;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
@@ -13,6 +14,7 @@ pub struct Email {
pub value: String,
pub unscoped: bool,
pub valid: Option<bool>,
pub displayname: Option<String>,
}
impl Model for Email {
@@ -116,6 +118,26 @@ impl Scopable for Email {
}
}
impl Email {
fn breaches(&self, db: &Database) -> Result<Vec<(Breach, Option<String>)>> {
use std::result;
let breach_id_pws = BreachEmail::belonging_to(self)
.select((breach_emails::breach_id, breach_emails::password))
.load::<(i32, Option<String>)>(db.db())?;
breach_id_pws.into_iter()
.map(|(breach_id, password)| {
breaches::table
.filter(breaches::id.eq(breach_id))
.first::<Breach>(db.db())
.map(|breach| (breach, password))
})
.collect::<result::Result<Vec<_>, _>>()
.map_err(Error::from)
}
}
pub struct PrintableEmail {
value: String,
}
@@ -134,9 +156,26 @@ impl Printable<PrintableEmail> for Email {
}
}
pub struct BreachWithPassword {
breach: PrintableBreach,
password: Option<String>,
}
impl fmt::Display for BreachWithPassword {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{}", self.breach)?;
if let Some(password) = &self.password {
write!(w, " ({:?})", password)?;
}
Ok(())
}
}
pub struct DetailedEmail {
id: i32,
value: String,
displayname: Option<String>,
breaches: Vec<BreachWithPassword>,
unscoped: bool,
valid: Option<bool>,
}
@@ -152,21 +191,27 @@ impl DisplayableDetailed for DetailedEmail {
w.id(self.id)?;
w.debug::<Green, _>(&self.value)?;
w.start_group();
w.opt_debug::<Yellow, _>(&self.displayname)?;
if let Some(valid) = self.valid {
write!(w, " [")?;
if valid {
w.display::<Green, _>("valid")?;
} else {
w.display::<Red, _>("invalid")?;
}
write!(w, "]")?;
}
w.end_group()?;
Ok(())
}
#[inline]
fn children(&self, _w: &mut fmt::DetailFormatter) -> fmt::Result {
fn children(&self, w: &mut fmt::DetailFormatter) -> fmt::Result {
for breach in &self.breaches {
w.child(breach)?;
}
Ok(())
}
}
@@ -176,26 +221,36 @@ display_detailed!(DetailedEmail);
impl Detailed for Email {
type T = DetailedEmail;
fn detailed(&self, _db: &Database) -> Result<Self::T> {
fn detailed(&self, db: &Database) -> Result<Self::T> {
let breaches = self.breaches(db)?.into_iter()
.map(|(sd, password)| Ok(BreachWithPassword {
breach: sd.printable(db)?,
password,
}))
.collect::<Result<_>>()?;
Ok(DetailedEmail {
id: self.id,
value: self.value.to_string(),
displayname: self.displayname.clone(),
breaches,
unscoped: self.unscoped,
valid: self.valid,
})
}
}
#[derive(Insertable)]
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
#[table_name="emails"]
pub struct NewEmail<'a> {
pub value: &'a str,
pub struct NewEmail {
pub value: String,
pub displayname: Option<String>,
pub valid: Option<bool>,
}
impl<'a> InsertableStruct<Email> for NewEmail<'a> {
impl InsertableStruct<Email> for NewEmail {
fn value(&self) -> &str {
self.value
&self.value
}
fn insert(&self, db: &Database) -> Result<()> {
@@ -206,25 +261,19 @@ impl<'a> InsertableStruct<Email> for NewEmail<'a> {
}
}
impl<'a> Upsertable<Email> for NewEmail<'a> {
impl Upsertable<Email> for NewEmail {
type Update = EmailUpdate;
fn upsert(self, existing: &Email) -> Self::Update {
Self::Update {
id: existing.id,
displayname: Self::upsert_opt(self.displayname, &existing.displayname),
valid: Self::upsert_opt(self.valid, &existing.valid),
}
}
}
#[derive(Debug, Insertable, Serialize, Deserialize)]
#[table_name="emails"]
pub struct NewEmailOwned {
pub value: String,
pub valid: Option<bool>,
}
impl Printable<PrintableEmail> for NewEmailOwned {
impl Printable<PrintableEmail> for NewEmail {
fn printable(&self, _db: &Database) -> Result<PrintableEmail> {
Ok(PrintableEmail {
value: self.value.to_string(),
@@ -232,12 +281,12 @@ impl Printable<PrintableEmail> for NewEmailOwned {
}
}
pub type InsertEmail = NewEmailOwned;
pub type InsertEmail = NewEmail;
impl LuaInsertToNewOwned for InsertEmail {
type Target = NewEmailOwned;
impl LuaInsertToNew for InsertEmail {
type Target = NewEmail;
fn try_into_new(self) -> Result<NewEmailOwned> {
fn try_into_new(self, _state: &Arc<State>) -> Result<NewEmail> {
Ok(self)
}
}
@@ -246,12 +295,14 @@ impl LuaInsertToNewOwned for InsertEmail {
#[table_name="emails"]
pub struct EmailUpdate {
pub id: i32,
pub displayname: Option<String>,
pub valid: Option<bool>,
}
impl Upsert for EmailUpdate {
fn is_dirty(&self) -> bool {
self.valid.is_some()
self.displayname.is_some() ||
self.valid.is_some()
}
fn generic(self) -> Update {
@@ -265,10 +316,12 @@ impl Upsert for EmailUpdate {
impl Updateable<Email> for EmailUpdate {
fn changeset(&mut self, existing: &Email) {
Self::clear_if_equal(&mut self.displayname, &existing.displayname);
Self::clear_if_equal(&mut self.valid, &existing.valid);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "displayname", &self.displayname);
Self::push_value(updates, "valid", &self.valid);
}
}

Some files were not shown because too many files have changed in this diff Show More