78 Commits

Author SHA1 Message Date
kpcyrd
309be4b22f Release v0.20.0 2020-12-28 20:47:45 +01:00
kpcyrd
02d0ccce64 Merge pull request #194 from kpcyrd/bump
Bump dependencies
2020-12-28 19:03:23 +00:00
kpcyrd
a15d8167b9 Fix clippy warnings 2020-12-28 18:12:45 +01:00
kpcyrd
d125d91f0d Update dependencies 2020-12-28 17:13:40 +01:00
kpcyrd
01e4f87420 Update dependencies 2020-12-28 15:58:10 +01:00
kpcyrd
037189ec57 Merge pull request #189 from kpcyrd/redirects
Add option to follow redirects
2020-12-28 13:17:22 +00:00
kpcyrd
0d77c06a95 Fix default user agent 2020-11-01 23:20:28 +01:00
kpcyrd
247648aff8 Add redirect support 2020-11-01 23:04:06 +01:00
kpcyrd
f73c375d12 Merge pull request #187 from kpcyrd/add-stdin
Allow streaming some structs from stdin into db
2020-11-01 20:11:59 +00:00
kpcyrd
484507e033 Merge pull request #188 from kpcyrd/inverse-rules
Add inverse rules
2020-11-01 20:04:42 +00:00
kpcyrd
27588f5319 Allow streaming some structs from stdin into db 2020-11-01 19:28:50 +01:00
kpcyrd
c2b535eeed Merge pull request #186 from kpcyrd/bump
Bump dependencies
2020-11-01 16:33:44 +00:00
kpcyrd
410a381643 Update kali install instructions 2020-11-01 03:46:56 +01:00
kpcyrd
f702f48708 Update dependencies 2020-10-27 00:56:26 +01:00
kpcyrd
031a269de0 Update cert parser 2020-10-27 00:49:34 +01:00
kpcyrd
c1b38c8fa6 Add inverse rules 2020-10-27 00:07:00 +01:00
kpcyrd
afe302c101 Update dependencies 2020-10-02 21:23:28 +02:00
kpcyrd
b09c820a11 Merge pull request #183 from kpcyrd/stealth
Add support to track module stealth in metadata
2020-09-25 21:20:32 +00:00
kpcyrd
301ad3cf44 Show possible stealth values in help 2020-09-25 22:51:03 +02:00
kpcyrd
53b1e9fd1a Align pkg-search style with pkg-list 2020-09-25 22:41:33 +02:00
kpcyrd
5c447d259f Update dependencies 2020-09-25 20:50:14 +02:00
kpcyrd
67082a1002 Allow filtering by stealth level in pkg list 2020-09-13 22:40:42 +02:00
kpcyrd
152dd82848 Show stealth status in pkg list 2020-09-13 22:19:01 +02:00
kpcyrd
21c70aaf58 Add author and repository meta fields 2020-09-13 21:36:33 +02:00
kpcyrd
a93d9ddbe2 Add -- Stealth metadata option 2020-09-13 21:24:17 +02:00
kpcyrd
1de7f1a2d2 Highlight weekends in cal time/hour view 2020-07-08 02:21:41 +02:00
kpcyrd
4a82171ca1 Correctly clear terminal after worker::spawn_multi 2020-06-26 03:18:11 +02:00
kpcyrd
1828b67509 Release v0.19.1 2020-06-24 00:46:45 +02:00
kpcyrd
8f2d71e631 Merge pull request #181 from kpcyrd/notify-fix
Fix early exit on notification and make ratelimiter more global
2020-06-23 23:11:59 +02:00
kpcyrd
2277089bda Share the ratelimit state between main modules and all notifications 2020-06-23 05:41:24 +02:00
kpcyrd
63f226f68f Prevent notifications from interfering with ctrl-c register 2020-06-23 04:55:16 +02:00
kpcyrd
da0e6aa482 Release v0.19.0 2020-06-18 21:06:11 +02:00
kpcyrd
82a2bb1bd0 Merge pull request #180 from kpcyrd/exit
Make sn0int more forgiving with accidential ^C
2020-06-18 18:46:29 +02:00
kpcyrd
b96eea97ee Add exit and quit to completion, remove quickstart 2020-06-18 03:38:16 +02:00
kpcyrd
fd420f4107 Make sn0int more forgiving with accidential ^C 2020-06-18 03:38:16 +02:00
kpcyrd
c5a23a5929 Merge pull request #179 from kpcyrd/notify
Trigger events on database insert/update/delete
2020-06-13 16:06:22 +02:00
kpcyrd
eb2e6203be Trigger notification on ttl expire 2020-06-13 00:48:11 +02:00
kpcyrd
b838dc5b31 Refactor db add/update logging code 2020-06-12 23:24:24 +02:00
kpcyrd
983df4a12e Trigger events on database inserts and updates 2020-06-12 21:32:15 +02:00
kpcyrd
9825bad1fe Merge pull request #178 from kpcyrd/notify
Add basic notification system
2020-06-12 04:13:09 +02:00
kpcyrd
0183e5dbcf Document signal notifications 2020-06-12 03:21:46 +02:00
kpcyrd
dacc28e2f1 Document discord notifications 2020-06-12 00:02:22 +02:00
kpcyrd
90b3abc471 Allow access to debug logs in notify scripts 2020-06-11 23:57:53 +02:00
kpcyrd
09333ebd0d Fix timer description 2020-06-11 04:10:11 +02:00
kpcyrd
e8b1b1ac87 Document notifications with pushover 2020-06-11 03:43:51 +02:00
kpcyrd
eb56a66b20 Include example code to write scripts with options 2020-06-11 03:23:37 +02:00
kpcyrd
fbd9909fef Document new notification system 2020-06-11 03:15:24 +02:00
kpcyrd
51e76b4c89 Connect db_activity to notification system 2020-06-11 01:51:59 +02:00
kpcyrd
484a426834 Fix workspace filter 2020-06-10 03:25:40 +02:00
kpcyrd
31b8840f8c Implement glob patterns for topics 2020-06-10 03:11:25 +02:00
kpcyrd
dfc13be9cc Add notify system 2020-06-10 01:53:43 +02:00
kpcyrd
d6bb6a9a1f Merge pull request #176 from kpcyrd/seccomp
Fix seccomp issues on arm
2020-06-09 13:51:39 +02:00
kpcyrd
bfbe8f2c1a Merge pull request #177 from kpcyrd/cal
Implement calendar function
2020-06-09 13:51:31 +02:00
kpcyrd
289b0edbb3 Fix seccomp issues on arm 2020-06-09 01:26:33 +02:00
kpcyrd
b69e5f879b Merge pull request #175 from kpcyrd/bump-deps
Bump dependencies
2020-06-08 14:40:12 +02:00
kpcyrd
d8810a449c Implement an hourly-view 2020-06-08 03:24:26 +02:00
kpcyrd
bdcd052500 Add time view with 12min slices 2020-06-08 03:24:26 +02:00
kpcyrd
26aa17bf4d Fix crash with low activity 2020-06-08 03:24:26 +02:00
kpcyrd
7a6d6cf2d5 Integrate activity into calendar 2020-06-08 03:24:26 +02:00
kpcyrd
e9cdc40821 Implement activity annotation with dummy data 2020-06-08 03:24:26 +02:00
kpcyrd
c4c7b420ce Implement calendar function 2020-06-08 03:24:26 +02:00
kpcyrd
3d304f13bc Bump dependencies 2020-06-08 03:21:22 +02:00
kpcyrd
6ee61c189e Merge pull request #174 from kpcyrd/cargo-deb
Add cargo-deb metadata
2020-06-08 03:19:17 +02:00
kpcyrd
5ad5666caf Merge pull request #173 from kpcyrd/docs
Update install instructions
2020-06-08 01:46:47 +02:00
kpcyrd
e057f8e6be Merge pull request #172 from kpcyrd/seccomp
Fix seccomp issue: clock_nanosleep
2020-06-08 01:45:31 +02:00
kpcyrd
baf44b4882 Add cargo-deb metadata 2020-06-08 01:43:51 +02:00
kpcyrd
f7fda8de4c Fix typo in sandbox docs 2020-06-07 23:59:39 +02:00
kpcyrd
43154cf841 Update install instructions 2020-06-07 23:59:39 +02:00
kpcyrd
49f082875d Update syscallz dependency 2020-06-07 23:13:48 +02:00
kpcyrd
1e575e0dbe Fix seccomp issue: clock_nanosleep 2020-06-07 23:13:48 +02:00
kpcyrd
d741020ff8 Merge pull request #168 from 0x646e78/fedora-install-notes
Add install details for Fedora/RH based systems
2020-06-06 14:33:11 +02:00
DNX
33dca47b38 Add install details for Fedora/RH based systems 2020-05-25 10:20:49 +10:00
kpcyrd
33bd4f9e94 Merge pull request #167 from kpcyrd/agent
Improve `pkg quickstart`, deprecate `mod`
2020-05-09 03:07:35 +02:00
kpcyrd
7a75a7a255 Prevent insertion of domains/subdomains with asterisks 2020-05-05 14:09:34 +02:00
kpcyrd
1c23995c86 Bump dependencies 2020-04-19 15:59:37 +02:00
kpcyrd
23ae7491e3 Make pkg quickstart skip already installed modules 2020-04-01 00:30:07 +02:00
kpcyrd
487578f974 Add deprecation notice for mod command 2020-03-31 23:44:03 +02:00
kpcyrd
4dec06843f Include os version into api useragent 2020-03-24 02:26:52 +01:00
123 changed files with 5828 additions and 3059 deletions

4642
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "sn0int"
version = "0.18.2"
version = "0.20.0"
description = "Semi-automatic OSINT framework and package manager"
authors = ["kpcyrd <git@rxv.cc>"]
license = "GPL-3.0"
@@ -17,71 +17,87 @@ members = ["sn0int-common",
"sn0int-registry",
"sn0int-std"]
[package.metadata.deb]
extended-description = """\
sn0int (pronounced /snoɪnt/) is a semi-automatic OSINT framework and package
manager. It was built for IT security professionals and bug hunters to gather
intelligence about a given target or about yourself. sn0int is enumerating
attack surface by semi-automatically processing public information and mapping
the results in a unified format for followup investigations."""
section = "utils"
priority = "optional"
depends = "$auto, publicsuffix"
build-depends = "pkg-config, libsqlite3-dev, libseccomp-dev, libsodium-dev, publicsuffix"
assets = [
["target/release/sn0int", "usr/bin/", "755"],
]
[features]
sqlite-bundled = ["libsqlite3-sys/bundled"]
[dependencies]
sn0int-common = { version="0.10.0", path="sn0int-common" }
sn0int-std = { version="=0.18.2", path="sn0int-std" }
rustyline = "6.0"
sn0int-common = { version="0.12.0", path="sn0int-common" }
sn0int-std = { version="=0.20.0", path="sn0int-std" }
rustyline = "7.0"
log = "0.4"
env_logger = "0.7"
env_logger = "0.8"
hlua-badtouch = "0.4"
structopt = "0.3"
failure = "0.1"
rand = "0.7"
colored = "1.6"
rand = "0.8"
colored = "2"
lazy_static = "1.0"
shellwords = "1.0"
diesel = { version = "1.0.0", features = ["sqlite", "chrono"] }
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
libsqlite3-sys = "0.16.0"
libsqlite3-sys = { version = "0.18.0", features = ["bundled-windows"] }
chrono = { version = "0.4", features = ["serde"] }
dirs = "2.0"
dirs-next = "2.0"
url = "2.0"
percent-encoding = "2.1"
#chrootable-https = { path = "../chrootable-https" }
chrootable-https = "0.15.2"
base64 = "0.12"
chrootable-https = "0.16"
base64 = "0.13"
data-encoding = "2.1.2"
serde_urlencoded = "0.6"
serde_urlencoded = "0.7"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
crossbeam-channel = "0.4"
crossbeam-channel = "0.5"
ctrlc = "3.1"
opener = "0.4"
separator = "0.4"
maplit = "1.0.1"
sloppy-rfc4880 = "0.1.5"
sloppy-rfc4880 = "0.2"
regex = "1.0"
toml = "0.5"
threadpool = "1.7"
atty = "0.2"
semver = "0.9"
semver = "0.11"
bytes = "0.4"
bytesize = "1.0"
ipnetwork = "0.16"
strum = "0.18"
strum_macros = "0.18"
ipnetwork = "0.17"
strum = "0.20"
strum_macros = "0.20"
embedded-triple = "0.1.0"
digest = "0.8.0"
md-5 = "0.8.0"
sha-1 = "0.8.1"
sha2 = "0.8.0"
sha3 = "0.8.0"
hmac = "0.7"
digest = "0.9"
md-5 = "0.9"
sha-1 = "0.9"
sha2 = "0.9"
sha3 = "0.9"
hmac = "0.10"
walkdir = "2.2"
nude = "0.3"
glob = "0.3.0"
os-version = "0.1.1"
[target.'cfg(target_os="linux")'.dependencies]
caps = "0.3"
caps = "0.5"
#syscallz = { path="../syscallz-rs" }
syscallz = "0.12"
nix = "0.17"
syscallz = "0.15"
nix = "0.19"
[target.'cfg(target_os="openbsd")'.dependencies]
pledge = "0.4"

View File

@@ -58,6 +58,39 @@ Mac OSX
brew install sn0int
Debian/Ubuntu/Kali
There are prebuilt packages signed by a debian maintainer. We can import the
key for this repository out of the debian keyring.
apt install debian-keyring
gpg -a --export --keyring /usr/share/keyrings/debian-maintainers.gpg git@rxv.cc | apt-key add -
apt-key adv --keyserver keyserver.ubuntu.com --refresh-keys git@rxv.cc
echo deb http://apt.vulns.sexy stable main > /etc/apt/sources.list.d/apt-vulns-sexy.list
apt update
apt install sn0int
Docker
docker run --rm --init -it -v "$PWD/.cache:/cache" -v "$PWD/.data:/data" kpcyrd/sn0int
Alpine
apk add sn0int
OpenBSD
pkg_add sn0int
Gentoo
layman -a pentoo
emerge --ask net-analyzer/sn0int
NixOS
nix-env -i sn0int
For everything else please have a look at the [detailed list][1].
[1]: https://sn0int.readthedocs.io/en/latest/install.html
@@ -67,11 +100,14 @@ For everything else please have a look at the [detailed list][1].
- [Installation](https://sn0int.readthedocs.io/en/latest/install.html)
- [Archlinux](https://sn0int.readthedocs.io/en/latest/install.html#archlinux)
- [Mac OSX](https://sn0int.readthedocs.io/en/latest/install.html#mac-osx)
- [Debian/Ubuntu/Kali](https://sn0int.readthedocs.io/en/latest/install.html#debian-ubuntu-kali)
- [Debian &gt;= bullseye, Ubuntu &gt;= 20.04, Kali](https://sn0int.readthedocs.io/en/latest/install.html#debian-bullseye-ubuntu-20-04-kali)
- [Debian &lt;= buster, Ubuntu &lt;= 19.10](https://sn0int.readthedocs.io/en/latest/install.html#debian-buster-ubuntu-19-10)
- [Fedora/CentOS/Redhat](https://sn0int.readthedocs.io/en/latest/install.html#fedora-centos-redhat)
- [Docker](https://sn0int.readthedocs.io/en/latest/install.html#docker)
- [Alpine](https://sn0int.readthedocs.io/en/latest/install.html#alpine)
- [OpenBSD](https://sn0int.readthedocs.io/en/latest/install.html#openbsd)
- [Gentoo](https://sn0int.readthedocs.io/en/latest/install.html#gentoo)
- [NixOS](https://sn0int.readthedocs.io/en/latest/install.html#nixos)
- [Windows](https://sn0int.readthedocs.io/en/latest/install.html#windows)
- [Build from source](https://sn0int.readthedocs.io/en/latest/build.html)
- [Install dependencies](https://sn0int.readthedocs.io/en/latest/build.html#install-dependencies)
@@ -129,6 +165,18 @@ For everything else please have a look at the [detailed list][1].
- [Logging events](https://sn0int.readthedocs.io/en/latest/activity.html#logging-events)
- [Querying events](https://sn0int.readthedocs.io/en/latest/activity.html#querying-events)
- [Visualization](https://sn0int.readthedocs.io/en/latest/activity.html#visualization)
- [Notifications](https://sn0int.readthedocs.io/en/latest/notifications.html)
- [Receiving notifications](https://sn0int.readthedocs.io/en/latest/notifications.html#receiving-notifications)
- [Telegram](https://sn0int.readthedocs.io/en/latest/notifications.html#telegram)
- [Pushover](https://sn0int.readthedocs.io/en/latest/notifications.html#pushover)
- [Discord](https://sn0int.readthedocs.io/en/latest/notifications.html#discord)
- [Signal](https://sn0int.readthedocs.io/en/latest/notifications.html#signal)
- [Writing your own module](https://sn0int.readthedocs.io/en/latest/notifications.html#writing-your-own-module)
- [Setting up notification rules](https://sn0int.readthedocs.io/en/latest/notifications.html#setting-up-notification-rules)
- [Testing notifications](https://sn0int.readthedocs.io/en/latest/notifications.html#testing-notifications)
- [Running sn0int automatically](https://sn0int.readthedocs.io/en/latest/notifications.html#running-sn0int-automatically)
- [Monitors](https://sn0int.readthedocs.io/en/latest/notifications.html#monitors)
- [Timers](https://sn0int.readthedocs.io/en/latest/notifications.html#timers)
- [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)

View File

@@ -19,8 +19,8 @@ case "$1" in
cargo test --verbose -- --ignored
;;
windows)
cargo build --verbose --features=sqlite-bundled
cargo build --verbose --examples --features=sqlite-bundled
cargo build --verbose
cargo build --verbose --examples
;;
boxxy)
cargo build --verbose --examples

View File

@@ -73,10 +73,4 @@ After all dependencies have been installed, simply build the binary:
$ cargo build --release
Note that you need a different command on windows:
.. code-block:: bash
$ cargo build --release --features=sqlite-bundled
After the build finished the binary is located at ``target/release/sn0int``.

View File

@@ -43,6 +43,7 @@ Getting Started
database
structs
activity
notifications
keyring
config
sandbox

View File

@@ -19,12 +19,37 @@ Mac OSX
$ brew install sn0int
Debian/Ubuntu/Kali
------------------
Debian >= bullseye, Ubuntu >= 20.04, 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.
There are prebuilt packages signed by a debian maintainer. We can import the
key for this repository out of the debian keyring.
.. code-block:: bash
$ sudo apt install debian-keyring
$ gpg -a --export --keyring /usr/share/keyrings/debian-maintainers.gpg kpcyrd@archlinux.org | sudo tee /etc/apt/trusted.gpg.d/apt-vulns-sexy.gpg
$ echo deb http://apt.vulns.sexy stable main | sudo tee /etc/apt/sources.list.d/apt-vulns-sexy.list
$ sudo apt update
$ sudo apt install sn0int
Debian <= buster, Ubuntu <= 19.10
---------------------------------
There are prebuilt packages signed by a debian maintainer. We can import the
key for this repository out of the debian keyring.
.. code-block:: bash
$ sudo apt install debian-keyring
$ gpg -a --export --keyring /usr/share/keyrings/debian-maintainers.gpg git@rxv.cc | sudo apt-key add -
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --refresh-keys git@rxv.cc
$ echo deb http://apt.vulns.sexy stable main | sudo tee /etc/apt/sources.list.d/apt-vulns-sexy.list
$ sudo apt update
$ sudo apt install sn0int
Fedora/CentOS/Redhat
--------------------
Using rust+cargo from the repos might work for you, but we only officially
support rust+cargo installed with `rustup <https://rustup.rs/>`_. Have a look
@@ -32,7 +57,7 @@ at the docker image as an alternative.
.. code-block:: bash
$ apt install build-essential libsqlite3-dev libseccomp-dev libsodium-dev publicsuffix
$ dnf install @development-tools libsq3-devel libseccomp-devel libsodium-devel publicsuffix-list
$ git clone https://github.com/kpcyrd/sn0int.git
$ cd sn0int
$ cargo install -f --path .
@@ -63,8 +88,15 @@ Gentoo
.. code-block:: bash
layman -f -o https://raw.githubusercontent.com/kpcyrd/overlay/master/overlay.xml -a kpcyrd-overlay
emerge --ask net-analyzer/sn0int
$ layman -a pentoo
$ emerge --ask net-analyzer/sn0int
NixOS
-----
.. code-block:: bash
$ nix-env -i sn0int
Windows
-------
@@ -78,4 +110,4 @@ Make sure rust is installed and setup.
$ git clone https://github.com/kpcyrd/sn0int.git
$ cd sn0int
$ cargo install -f --path . --features=sqlite-bundled
$ cargo install -f --path .

311
docs/notifications.rst Normal file
View File

@@ -0,0 +1,311 @@
Notifications
=============
If you run sn0int unattended nobody might see the sn0int output. For cases like
this you can configure notifications to send you a push notification in case
something interesting happens. This is also especially useful if you have
sn0int setup to run automatically.
Receiving notifications
-----------------------
Notifications are just regular sn0int modules. You can install them just like
any other module or write your own. This section contains walkthroughs on how
to setup common integrations.
Telegram
~~~~~~~~
Install the telegram notification module from the registry:
.. code-block:: bash
sn0int pkg install kpcyrd/notify-telegram
Open your telegram app and open a chat with ``@botfather``. Send ``/newbot``
and answer the questions. Copy ``bot_token`` and open this url in your browser:
.. code-block::
https://api.telegram.org/bot**your_bot_token**/getUpdates
Back on your app, open the t.me link to start a new chat with your bot, then
send ``/start``. Reload the page in your browser, you should see the new
message you sent. Copy the ``chat_id``.
Test your tokens are working correctly by sending yourself a notification:
.. code-block:: bash
sn0int notify exec kpcyrd/notify-telegram -o bot_token=1337:foobar -o chat_id=1337 'hello world'
You should receive ``hello world`` from your bot on Telegram.
Pushover
~~~~~~~~
Install the pushover notification module from the registry:
.. code-block:: bash
sn0int pkg install kpcyrd/notify-pushover
Signup for pushover and configure the app on your device. Copy th user key
visible on the pushover dashboard. Click "Create an Application/API Token". Set
"sn0int" as name and set an icon if you want to. Copy the api token.
Test your tokens are working correctly by sending yourself a notification:
.. code-block:: bash
sn0int notify exec kpcyrd/notify-pushover -o user_key=asdf1337 -o api_token=asdf1337 'hello world'
You should receive ``hello world`` as a push notification.
Discord
~~~~~~~
Install the discord notification module from the registry:
.. code-block:: bash
sn0int pkg install kpcyrd/notify-discord
Decide which channel should receive notifications (or create a new one). Open
the "Server Settings" of your discord server. Click on "Webhooks". Click
"Create Webhook". Configure the Name and Channel. Copy the Webhook URL.
Test your tokens are working correctly by sending yourself a notification:
.. code-block:: bash
sn0int notify exec kpcyrd/notify-discord -o url=https://discord.com/api/webhooks/1337/asdf 'hello world'
You should receive ``hello world`` in your discord channel.
Signal
~~~~~~
Install the sn0int notification module from the registry:
.. code-block:: bash
sn0int pkg install kpcyrd/notify-signal
This module allows end-to-end encrypted notifications, but it's also difficult
to setup. You need a second phone number and install both `signal-cli
<https://github.com/AsamK/signal-cli>`_ and `sn0int-signal
<https://github.com/kpcyrd/sn0int-signal>`_.
After you've registered your second phone number with signal-cli, you can use
sn0int-signal to expose a minimal api for notify-signal. For more detailed
instructions and how to start the api at boot, see the `sn0int-signal README
<https://github.com/kpcyrd/sn0int-signal>`_.
Read the secret key generated at ``/etc/sn0int-signal.key`` and send a
notification to the signal phone number:
.. code-block:: bash
sn0int notify exec kpcyrd/notify-signal -o to=+31337 -o secret=asdf 'hello world'
You should receive ``hello world`` from the number signed up with signal-cli.
Writing your own module
~~~~~~~~~~~~~~~~~~~~~~~
Make sure you've read the detailed instructions on how to get setup with
`module development <scripting.html>`_.
Create a new sn0int module like this:
.. code-block:: bash
sn0int new ~/repos/sn0int-modules/notify-custom.lua
Edit the ``-- Source:`` so it takes notifications as input:
.. code-block:: lua
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: notifications
function run(arg)
-- TODO your code here
-- https://sn0int.readthedocs.io/en/stable/reference.html
debug(arg)
info(arg['subject'])
info(arg['body'])
end
Execute your script:
.. code-block:: bash
sn0int notify exec notify-custom 'hello world'
You most likely need to pass options to avoid hard-coding keys into your
script. Options can be fetched like this:
.. code-block:: lua
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: notifications
function run(arg)
-- TODO your code here
-- https://sn0int.readthedocs.io/en/stable/reference.html
local foo = getopt('foo')
if not foo then return 'Missing -o foo= option' end
info('foo: ' .. foo)
info('subject: ' .. arg['subject'])
end
And passed like this:
.. code-block:: bash
sn0int notify exec notify-custom -o "foo=hello world" 'ohai'
Setting up notification rules
-----------------------------
We now know how to trigger notifications manually, but we would rather trigger
notifications if a module runs into something interesting.
You can setup subscriptions on specific topics and then have a notification
script execute automatically.
Lookup the location of your sn0int config file:
.. code-block:: bash
sn0int paths
And open it in an editor of your choice:
.. code-block:: bash
vim /home/user/.config/sn0int.toml
A basic configuration could look like this:
.. code-block:: toml
# You can have multiple notification sections, this one is named
# `demo-telegram-integration`
# The label can be set to whatever you want, but you may need to add
# double-quotes to use some characters.
[notifications.demo-telegram-integration]
# If this option is present, the notification must originate from one of
# the following workspaces.
workspaces = ["default", "some-workspace"]
# If this option is present, the notification must match one of the
# filters. You can use `*` as a wildcard to match everything except `:`.
topics = ["activity:harness/activity-ping:*"]
# Mandatory: the module to execute.
script = "kpcyrd/notify-telegram"
# The options to pass to the module, if any.
# Can be accessed with `getopt`
options = [
"bot_token=1337:foobar",
"chat_id=1337",
]
All options except ``script`` are optional, but setting filters is highly
recommended.
Testing notifications
---------------------
To test if your configuration works correctly you can create an event manually:
.. code-block:: bash
sn0int -w some-workspace notify send activity:harness/activity-ping:dummy "hello world"
If it matches any of your rules you should receive a push notifications.
.. note::
If you want to test just the routing without actually sending something, add ``--dry-run``.
Running sn0int automatically
----------------------------
Support for this is going to improve in the future, but you can already set
this up if you're ok with a slightly buggy experience.
Monitors
~~~~~~~~
Some modules are long-running and either wait for an event from a server or
have custom polling built in that's usually configurable with an ``-o
interval=`` option. If your module has a non-trivial setup phase, an author may
take this approach.
.. code-block::
# /etc/systemd/system/sn0int-your-new-service.service
[Unit]
Description=sn0int: run example/changeme
[Service]
User=your-user
ExecStart=/usr/bin/sn0int run -w your-workspace example/changeme
Restart=always
RestartSec=0
[Install]
WantedBy=multi-user.target
Enable the service to run on boot:
.. code-block:: bash
systemctl enable --now sn0int-your-new-service.service
Timers
~~~~~~
If the module is only one-shot you can set it up to run with a timer:
.. code-block::
# /etc/systemd/system/sn0int-your-other-service.service
[Unit]
Description=sn0int: run example/changeme
[Service]
User=your-user
ExecStart=/usr/bin/sn0int run -w your-workspace example/changeme
Setup the timer like this:
.. code-block::
# /etc/systemd/system/sn0int-your-other-service.timer
[Unit]
Description=sn0int: run example/changeme
[Timer]
OnBootSec=1min
OnUnitActiveSec=1h
[Install]
WantedBy=timers.target
.. code-block:: bash
systemctl enable --now sn0int-your-other-service.timer

View File

@@ -383,6 +383,10 @@ options are set. The following options are available:
The request body that should be json encoded.
``form``
The request body that should be form encoded.
``follow_redirects``
Automatically follow redirects, up to the specified number. If set to 1, only
one redirect is going to be followed. Defaults to 0 so redirects aren't
followed.
``body``
The raw request body as string.
``into_blob``

View File

@@ -6,7 +6,7 @@ child process. It's important to note that there's a basic sandbox that's
active on every operating system, and there's a second line of defense on
supported operating systems.
The first line of defense is the restrictive stdlib. It's assumed that and
The first line of defense is the restrictive stdlib. It's assumed that an
attacker gains full control over the lua code and is able to call any function
with arbitrary arguments. The stdlib only provides functions that are
considered safe, so for example it's not possible to start a process or open a

View File

@@ -0,0 +1,18 @@
PRAGMA foreign_keys=off;
CREATE TABLE _ttls_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
family VARCHAR NOT NULL,
key INTEGER NOT NULL,
expire DATETIME NOT NULL,
CONSTRAINT ttl_unique UNIQUE (family, key)
);
INSERT INTO _ttls_new (id, family, key, expire)
SELECT id, family, key, expire
FROM ttls;
DROP TABLE ttls;
ALTER TABLE _ttls_new RENAME TO ttls;
PRAGMA foreign_keys=on;

View File

@@ -0,0 +1,19 @@
PRAGMA foreign_keys=off;
CREATE TABLE _ttls_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
family VARCHAR NOT NULL,
key INTEGER NOT NULL,
value VARCHAR NOT NULL,
expire DATETIME NOT NULL,
CONSTRAINT ttl_unique UNIQUE (family, key)
);
INSERT INTO _ttls_new (id, family, key, value, expire)
SELECT id, family, key, "unknown", expire
FROM ttls;
DROP TABLE ttls;
ALTER TABLE _ttls_new RENAME TO ttls;
PRAGMA foreign_keys=on;

View File

@@ -0,0 +1,26 @@
-- Description: Log some dummy activity
-- Version: 0.1.0
-- License: GPL-3.0
function run()
local uniq = getopt('uniq')
local topic = getopt('topic') or 'harness/activity-ping:dummy'
if getopt('gps') then
lat=1.23
lon=4.56
radius=100
end
db_activity({
topic=topic,
time=sn0int_time(),
uniq=uniq,
latitude=lat,
longitude=lon,
radius=radius,
content={
msg='ohai',
},
})
end

View File

@@ -0,0 +1,26 @@
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- git log -s --format='%H %ci'
function run()
while true do
local x = stdin_readline()
if x == nil then
break
end
local m = regex_find('^(\\S+) (.+)', x)
if m then
time = strptime('%Y-%m-%d %T %z', m[3])
time = sn0int_time_from(time)
db_activity({
topic='harness/sn0int-commit:dummy',
time=time,
uniq=m[2],
content={},
})
end
end
end

View File

@@ -0,0 +1,10 @@
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: subdomains
function run(arg)
db_update('subdomain', arg, {
resolvable=true,
})
end

View File

@@ -0,0 +1,13 @@
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: domains
function run(arg)
for i=1, 20 do
db_add('subdomain', {
domain_id=arg['id'],
value=http_mksession() .. '.' .. arg['value'],
})
end
end

View File

@@ -0,0 +1,8 @@
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: notifications
function run(arg)
info('notication!: ' .. json_encode(arg))
end

View File

@@ -0,0 +1,9 @@
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: notifications
function run(arg)
ratelimit_throttle('foo', 3, 10000)
info('notication!: ' .. json_encode(arg))
end

View File

@@ -0,0 +1,10 @@
-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
-- Source: notifications
function run(arg)
-- TODO your code here
sleep(1)
info('notication!: ' .. json_encode(arg))
end

View File

@@ -1,5 +1,6 @@
-- Description: Send a request to a hidden service
-- Version: 0.1.0
-- Stealth: passive
-- License: GPL-3.0
function run()

View File

@@ -1,5 +1,6 @@
-- Description: basic selftest
-- Version: 0.1.0
-- Stealth: offline
-- License: GPL-3.0
function run()

View File

@@ -1,5 +1,6 @@
-- Description: Read from stdin
-- Version: 0.1.0
-- Stealth: offline
-- License: GPL-3.0
function run()

View File

@@ -1,6 +1,7 @@
-- Description: jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
-- Version: 0.1.0
-- License: GPL-3.0
-- Stealth: loud
-- Source: domains
function run()

View File

@@ -1,6 +1,6 @@
[package]
name = "sn0int-common"
version = "0.10.0"
version = "0.12.0"
description = "sn0int - common code"
authors = ["kpcyrd <git@rxv.cc>"]
license = "GPL-3.0"
@@ -8,9 +8,8 @@ repository = "https://github.com/kpcyrd/sn0int"
edition = "2018"
[dependencies]
serde = "1.0"
serde_derive = "1.0"
serde = { version = "1.0", features=["derive"] }
#rocket_failure_errors = { path = "../../../rocket_failure/rocket_failure_errors" }
rocket_failure_errors = "0.2"
failure = "0.1"
nom = "5.0"
anyhow = "1.0"
nom = "6.0"

View File

@@ -1,4 +1,5 @@
use crate::id::ModuleID;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct WhoamiResponse {

View File

@@ -1,2 +1,2 @@
pub use failure::{Error, ResultExt};
pub use anyhow::{Error, Context, anyhow, format_err, bail};
pub type Result<T> = ::std::result::Result<T, Error>;

View File

@@ -1,5 +1,4 @@
use crate::errors::*;
use nom;
use serde::{de, Serialize, Serializer, Deserialize, Deserializer};
use std::fmt;
use std::result;
@@ -53,7 +52,7 @@ impl FromStr for ModuleID {
fn from_str(s: &str) -> Result<ModuleID> {
let (trailing, module) = module(s)
.map_err(|err| format_err!("Failed to parse module id: {:?}", err))?;
.map_err(|err| anyhow!("Failed to parse module id: {:?}", err))?;
if !trailing.is_empty() {
bail!("Trailing data in module id");
}

View File

@@ -1,5 +1,3 @@
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate failure;
#[macro_use] extern crate nom;
pub mod api;

View File

@@ -1,7 +1,9 @@
use crate::errors::*;
use serde::{Serialize, Deserialize};
use std::str::FromStr;
mod stealth;
pub use self::stealth::Stealth;
#[derive(Debug, PartialEq, Clone)]
pub enum EntryType {
@@ -9,6 +11,9 @@ pub enum EntryType {
Version,
Source,
KeyringAccess,
Stealth,
Author,
Repository,
License,
}
@@ -21,6 +26,9 @@ impl FromStr for EntryType {
"Version" => Ok(EntryType::Version),
"Source" => Ok(EntryType::Source),
"Keyring-Access" => Ok(EntryType::KeyringAccess),
"Stealth" => Ok(EntryType::Stealth),
"Author" => Ok(EntryType::Author),
"Repository" => Ok(EntryType::Repository),
"License" => Ok(EntryType::License),
x => bail!("Unknown EntryType: {:?}", x),
}
@@ -44,6 +52,7 @@ pub enum Source {
Netblocks,
CryptoAddrs(Option<String>),
KeyRing(String),
Notifications,
}
impl Source {
@@ -63,6 +72,7 @@ impl Source {
Source::Ports => "ports",
Source::Netblocks => "netblocks",
Source::CryptoAddrs(_) => "cryptoaddrs",
Source::Notifications => "notifications",
Source::KeyRing(_) => "keyring",
}
}
@@ -94,6 +104,7 @@ impl FromStr for Source {
("ports", None) => Ok(Source::Ports),
("netblocks", None) => Ok(Source::Netblocks),
("cryptoaddrs", param) => Ok(Source::CryptoAddrs(param.map(String::from))),
("notifications", None) => Ok(Source::Notifications),
("keyring", Some(param)) => Ok(Source::KeyRing(param.to_string())),
(x, Some(param)) => bail!("Unknown Source: {:?} ({:?})", x, param),
(x, None) => bail!("Unknown Source: {:?}", x),
@@ -133,6 +144,9 @@ pub struct Metadata {
pub version: String,
pub source: Option<Source>,
pub keyring_access: Vec<String>,
pub stealth: Stealth,
pub authors: Vec<String>,
pub repository: Option<String>,
pub license: License,
}
@@ -151,6 +165,9 @@ impl FromStr for Metadata {
EntryType::Version => data.version = Some(v),
EntryType::Source => data.source = Some(v),
EntryType::KeyringAccess => data.keyring_access.push(v),
EntryType::Stealth => data.stealth = Some(v),
EntryType::Author => data.authors.push(v),
EntryType::Repository => data.repository = Some(v),
EntryType::License => data.license = Some(v),
}
}
@@ -165,6 +182,9 @@ pub struct NewMetadata<'a> {
pub version: Option<&'a str>,
pub source: Option<&'a str>,
pub keyring_access: Vec<&'a str>,
pub stealth: Option<&'a str>,
pub authors: Vec<&'a str>,
pub repository: Option<&'a str>,
pub license: Option<&'a str>,
}
@@ -179,6 +199,14 @@ impl<'a> NewMetadata<'a> {
let keyring_access = self.keyring_access.into_iter()
.map(String::from)
.collect();
let stealth = match self.stealth {
Some(x) => x.parse()?,
_ => Stealth::Normal,
};
let authors = self.authors.into_iter()
.map(String::from)
.collect();
let repository = self.repository.map(String::from);
let license = self.license.ok_or_else(|| format_err!("License is required"))?;
let license = license.parse()?;
@@ -187,6 +215,9 @@ impl<'a> NewMetadata<'a> {
version: version.to_string(),
source,
keyring_access,
stealth,
authors,
repository,
license,
})
}
@@ -227,6 +258,36 @@ mod tests {
version: "1.0.0".to_string(),
license: License::WTFPL,
source: Some(Source::Domains),
stealth: Stealth::Normal,
authors: vec![],
repository: None,
keyring_access: Vec::new(),
});
}
#[test]
fn verify_much_metadata() {
let metadata = Metadata::from_str(r#"-- Description: Hello world, this is my description
-- Version: 1.0.0
-- Source: domains
-- Stealth: passive
-- Author: kpcyrd <git at rxv dot cc>
-- Author: kpcyrd's cat
-- Repository: https://github.com/kpcyrd/sn0int
-- License: WTFPL
"#).expect("parse");
assert_eq!(metadata, Metadata {
description: "Hello world, this is my description".to_string(),
version: "1.0.0".to_string(),
license: License::WTFPL,
source: Some(Source::Domains),
stealth: Stealth::Passive,
authors: vec![
"kpcyrd <git at rxv dot cc>".to_string(),
"kpcyrd's cat".to_string(),
],
repository: Some("https://github.com/kpcyrd/sn0int".to_string()),
keyring_access: Vec::new(),
});
}
@@ -243,6 +304,9 @@ mod tests {
version: "1.0.0".to_string(),
license: License::WTFPL,
source: None,
stealth: Stealth::Normal,
authors: vec![],
repository: None,
keyring_access: Vec::new(),
});
}

View File

@@ -0,0 +1,53 @@
use crate::errors::*;
use serde::{Serialize, Deserialize};
use std::str::FromStr;
#[derive(Debug, Eq, PartialEq, PartialOrd, Clone, Serialize, Deserialize)]
pub enum Stealth {
Loud,
Normal,
Passive,
Offline,
}
impl Stealth {
#[inline]
pub fn variants() -> &'static [&'static str] {
&[
"loud",
"normal",
"passive",
"offline",
]
}
#[inline(always)]
fn as_num(&self) -> u8 {
match self {
Stealth::Loud => 3,
Stealth::Normal => 2,
Stealth::Passive => 1,
Stealth::Offline => 0,
}
}
#[inline]
pub fn equal_or_better(&self, other: &Self) -> bool {
self.as_num() <= other.as_num()
}
}
impl FromStr for Stealth {
type Err = Error;
fn from_str(s: &str) -> Result<Stealth> {
match s {
"loud" => Ok(Stealth::Loud),
// This is also the default level if none is provided
"normal" => Ok(Stealth::Normal),
"passive" => Ok(Stealth::Passive),
"offline" => Ok(Stealth::Offline),
x => bail!("Unknown stealth: {:?}", x),
}
}
}

View File

@@ -8,7 +8,7 @@ repository = "https://github.com/kpcyrd/sn0int"
edition = "2018"
[dependencies]
sn0int-common = { version="0.10.0", path="../sn0int-common" }
sn0int-common = { version="0.12.0", path="../sn0int-common" }
rocket = { version = "0.4", default-features=false }
#rocket_failure = { path = "../../rocket_failure" }
rocket_failure = { version = "0.2" }

View File

@@ -1,6 +1,6 @@
[package]
name = "sn0int-std"
version = "0.18.2"
version = "0.20.0"
description = "sn0int - stdlib"
authors = ["kpcyrd <git@rxv.cc>"]
repository = "https://github.com/kpcyrd/sn0int"
@@ -14,43 +14,43 @@ log = "0.4"
failure = "0.1"
hlua-badtouch = "0.4"
tokio = "0.1.14"
rand = "0.7"
rand = "0.8"
regex = "1.0"
rustls = { version="0.16", features=["dangerous_configuration"] }
rustls = { version="0.18", features=["dangerous_configuration"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.6"
serde_urlencoded = "0.7"
webpki = "0.21"
webpki-roots = "0.19.0"
ct-logs = "0.6"
webpki-roots = "0.20"
ct-logs = "0.7"
#chrootable-https = { path = "../../chrootable-https" }
chrootable-https = "0.15.2"
chrootable-https = "0.16"
http = "0.2"
bufstream = "0.1.4"
pem = "0.7"
pem = "0.8"
url = "2.0"
tungstenite = { version = "0.10.1", default-features = false }
tungstenite = { version = "0.11", default-features = false }
kuchiki = "0.8.0"
maxminddb = "0.13"
x509-parser = "0.6.2"
der-parser = "3.0"
maxminddb = "0.17"
x509-parser = "0.9"
der-parser = "5"
publicsuffix = { version="1.5", default-features=false }
xml-rs = "0.8"
geo = "0.12"
geo = "0.16"
bytes = "0.4"
base64 = "0.12"
base64 = "0.13"
chrono = { version = "0.4", features = ["serde"] }
mqtt-protocol = "0.8.1"
mqtt-protocol = "0.10"
sodiumoxide = { version="0.2.5", features=["use-pkg-config"] }
image = "0.23.0"
kamadak-exif = "0.5.1"
bs58 = "0.3"
digest = "0.8.0"
blake2 = "0.8.0"
bs58 = "0.4"
digest = "0.9"
blake2 = "0.9"
[dev-dependencies]
env_logger = "0.7"
env_logger = "0.8"
maplit = "1.0.1"

View File

@@ -1,6 +1,6 @@
use bytes::Bytes;
use blake2::VarBlake2b;
use digest::{Input, VariableOutput};
use digest::{Update, VariableOutput};
use serde::ser::{Serialize, Serializer};
use serde::de::{self, Deserialize, Deserializer};
use std::result;
@@ -22,8 +22,9 @@ impl Blob {
pub fn hash(bytes: &[u8]) -> String {
let mut h = VarBlake2b::new(32).unwrap();
h.input(bytes);
Self::encode_hash(&h.vec_result())
h.update(bytes);
let output = h.finalize_boxed();
Self::encode_hash(&output)
}
#[inline]

View File

@@ -1,78 +1,9 @@
use crate::errors::*;
use x509_parser;
use der_parser::der::DerObject;
use der_parser::ber::{BerObjectContent, BerTag};
use der_parser::oid::Oid;
use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Debug, PartialEq)]
pub enum AlternativeName {
DnsName(String),
Email(String),
IpAddr(IpAddr),
}
pub fn san_extension(i: &[u8]) -> Result<Vec<AlternativeName>> {
let (rem, seq) = der_parser::der::parse_der_sequence(i)
.map_err(|_| format_err!("Failed to parse san extension"))?;
if !rem.is_empty() {
bail!("san extension has trailing garbage");
}
debug!("Decoded sequence: {:?}", seq);
if let BerObjectContent::Sequence(seq) = seq.content {
seq.into_iter()
.map(san_value)
.collect()
} else {
bail!("Expected der sequence");
}
}
pub fn san_value(o: DerObject) -> Result<AlternativeName> {
debug!("DER object in SAN extension: {:?}", o);
match (o.class, o.tag, &o.content) {
(2, BerTag::Integer, BerObjectContent::Unknown(BerTag::Integer, value)) => san_value_dns(value),
(2, BerTag::Boolean, BerObjectContent::Unknown(BerTag::Boolean, value)) => san_value_email(value),
(2, BerTag::ObjDescriptor, BerObjectContent::Unknown(BerTag::ObjDescriptor, value)) => san_value_ipaddr(value),
_ => bail!("Unexpected object: {:?}", o),
}
}
pub fn san_value_dns(v: &[u8]) -> Result<AlternativeName> {
debug!("Reading as dns name: {:?}", v);
String::from_utf8(v.to_vec())
.map(AlternativeName::DnsName)
.map_err(Error::from)
}
pub fn san_value_email(v: &[u8]) -> Result<AlternativeName> {
debug!("Reading as email: {:?}", v);
String::from_utf8(v.to_vec())
.map(AlternativeName::Email)
.map_err(Error::from)
}
pub fn san_value_ipaddr(v: &[u8]) -> Result<AlternativeName> {
debug!("Reading as ipaddr: {:?}", v);
match v.len() {
4 => Ok(AlternativeName::IpAddr(Ipv4Addr::from([
v[0], v[1], v[2], v[3],
]).into())),
16 => Ok(AlternativeName::IpAddr(Ipv6Addr::from([
v[0], v[1], v[2], v[3],
v[4], v[5], v[6], v[7],
v[8], v[9], v[10], v[11],
v[12], v[13], v[14], v[15],
]).into())),
_ => Err(format_err!("Invalid ipaddr")),
}
}
use std::net::IpAddr;
use x509_parser::x509::X509Version;
use x509_parser::certificate::X509Certificate;
use x509_parser::extensions::{GeneralName, ParsedExtension};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Certificate {
@@ -83,7 +14,7 @@ pub struct Certificate {
impl Certificate {
pub fn parse_pem(crt: &str) -> Result<Certificate> {
let pem = match x509_parser::pem::pem_to_der(crt.as_bytes()) {
let pem = match x509_parser::pem::parse_x509_pem(crt.as_bytes()) {
Ok((remaining, pem)) => {
if !remaining.is_empty() {
bail!("input cert has trailing garbage");
@@ -99,12 +30,12 @@ impl Certificate {
}
pub fn from_bytes(crt: &[u8]) -> Result<Certificate> {
let crt = match x509_parser::parse_x509_der(&crt) {
let crt = match X509Certificate::from_der(&crt) {
Ok((remaining, der)) => {
if !remaining.is_empty() {
bail!("input cert has trailing garbage");
}
if der.tbs_certificate.version != 2 {
if der.tbs_certificate.version != X509Version::V3 {
bail!("unexpected certificate version");
}
der
@@ -116,35 +47,45 @@ impl Certificate {
let mut valid_emails = HashSet::new();
let mut valid_ipaddrs = HashSet::new();
for x in crt.tbs_certificate.subject.rdn_seq {
for y in x.set {
// CommonName
if y.attr_type != Oid::from(&[2, 5, 4, 3]) {
continue;
}
if let Ok(x) = y.attr_value.content.as_slice() {
let value = String::from_utf8(x.to_vec())?;
info!("Found CN in Subject: {:?}", value);
valid_names.insert(value);
}
for attr in crt.subject().iter_common_name() {
if let Ok(cn) = attr.as_str() {
info!("Found CN in Subject: {:?}", cn);
valid_names.insert(cn.to_string());
}
}
for x in crt.tbs_certificate.extensions {
if x.oid != Oid::from(&[2, 5, 29, 17]) {
continue;
}
debug!("Found san extension: {:?}", x.value);
let values = san_extension(x.value)?;
for v in values {
match v {
AlternativeName::DnsName(v) => valid_names.insert(v),
AlternativeName::Email(v) => valid_emails.insert(v),
AlternativeName::IpAddr(v) => valid_ipaddrs.insert(v),
};
for (_oid, ext) in crt.tbs_certificate.extensions {
if let ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() {
for name in &san.general_names {
debug!("Certificate is valid for {:?}", name);
match name {
GeneralName::DNSName(v) => {
valid_names.insert(v.to_string());
},
GeneralName::RFC822Name(v) => {
valid_emails.insert(v.to_string());
},
GeneralName::IPAddress(v) => {
let ip = match v.len() {
4 => Some(IpAddr::from([v[0], v[1], v[2], v[3]])),
16 => Some(IpAddr::from([
v[0], v[1], v[2], v[3],
v[4], v[5], v[6], v[7],
v[8], v[9], v[10], v[11],
v[12], v[13], v[14], v[15],
])),
_ => {
info!("Certificate is valid for invalid ip address: {:?}", v);
None
},
};
if let Some(ip) = ip {
valid_ipaddrs.insert(ip);
}
},
_ => (),
}
}
}
}
@@ -162,7 +103,6 @@ impl Certificate {
#[cfg(test)]
mod tests {
use super::*;
use der_parser::parse_der;
#[test]
fn test_parse_pem_github() {
@@ -260,19 +200,6 @@ ZkZZmqNn2Q8=
});
}
#[test]
fn test_san_extension() {
let ext = san_extension(&[48, 28,
130, 10, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109,
130, 14, 119, 119, 119, 46, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109
])
.expect("Failed to parse extension");
assert_eq!(ext, vec![
AlternativeName::DnsName(String::from("github.com")),
AlternativeName::DnsName(String::from("www.github.com")),
]);
}
#[test]
fn test_long_san_extension() {
let mut x = Certificate::parse_pem(r#"-----BEGIN CERTIFICATE-----
@@ -372,48 +299,6 @@ z/6Vy8Ga9kigYVsa8ZFMR+Ex
});
}
#[test]
fn test_san_value_dns() {
let (rem, v) = parse_der(&[130, 10, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109])
.expect("Failed to parse san value");
assert!(rem.is_empty());
println!("{:?}", v);
assert_eq!(v, DerObject {
class: 2,
structured: 0,
tag: BerTag::Integer,
content: BerObjectContent::Unknown(BerTag::Integer, &[103, 105, 116, 104, 117, 98, 46, 99, 111, 109])
});
let content = match v.content {
BerObjectContent::Unknown(BerTag::Integer, v) => v,
_ => panic!("Wrong BerObjectContent"),
};
let v = san_value_dns(content)
.expect("Failed to process san value");
assert_eq!(v, AlternativeName::DnsName(String::from("github.com")));
}
#[test]
fn test_san_value_ipaddr() {
let (rem, v) = parse_der(&[135, 4, 1, 1, 1, 1])
.expect("Failed to parse san value");
assert!(rem.is_empty());
println!("{:?}", v);
assert_eq!(v, DerObject {
class: 2,
structured: 0,
tag: BerTag::ObjDescriptor,
content: BerObjectContent::Unknown(BerTag::ObjDescriptor, &[1, 1, 1, 1])
});
let content = match v.content {
BerObjectContent::Unknown(BerTag::ObjDescriptor, v) => v,
_ => panic!("Wrong BerObjectContent"),
};
let v = san_value_ipaddr(content)
.expect("Failed to process san value");
assert_eq!(v, AlternativeName::IpAddr("1.1.1.1".parse().unwrap()));
}
#[test]
fn test_san_email() {
let mut x = Certificate::parse_pem(r#"-----BEGIN CERTIFICATE-----

View File

@@ -2,9 +2,6 @@ use crate::errors::*;
use crate::hlua::{AnyHashableLuaValue, AnyLuaValue, AnyLuaString};
use std::collections::{self, HashMap};
use crate::json::LuaJsonValue;
use serde;
use serde_json;
pub fn from_lua<T>(x: LuaJsonValue) -> Result<T>
where for<'de> T: serde::Deserialize<'de>

View File

@@ -3,14 +3,8 @@ use maxminddb::geoip2;
use std::collections::BTreeMap;
fn from_geoip_model_names(names: Option<BTreeMap<String, String>>) -> Option<String> {
let names = match names {
Some(names) => names,
_ => return None,
};
names.get("en")
.map(|x| x.to_owned())
fn from_geoip_model_names(names: Option<BTreeMap<&str, &str>>) -> Option<String> {
names?.get("en").map(|x| x.to_string())
}
#[derive(Debug, Serialize)]
@@ -24,7 +18,7 @@ pub struct GeoLookup {
pub longitude: Option<f64>,
}
impl From<geoip2::City> for GeoLookup {
impl<'a> From<geoip2::City<'a>> for GeoLookup {
fn from(lookup: geoip2::City) -> GeoLookup {
// parse maxminddb lookup
let continent = match lookup.continent {
@@ -79,17 +73,11 @@ pub struct Continent {
impl Continent {
fn from_maxmind(continent: geoip2::model::Continent) -> Option<Self> {
let code = match continent.code {
Some(code) => code,
_ => return None,
};
let name = match from_geoip_model_names(continent.names) {
Some(name) => name,
_ => return None,
};
let code = continent.code?;
let name = from_geoip_model_names(continent.names)?;
Some(Continent {
code,
code: code.to_string(),
name,
})
}
@@ -103,17 +91,11 @@ pub struct Country {
impl Country {
fn from_maxmind(country: geoip2::model::Country) -> Option<Self> {
let code = match country.iso_code {
Some(code) => code,
_ => return None,
};
let name = match from_geoip_model_names(country.names) {
Some(name) => name,
_ => return None,
};
let code = country.iso_code?;
let name = from_geoip_model_names(country.names)?;
Some(Country {
code,
code: code.to_string(),
name,
})
}
@@ -152,19 +134,14 @@ pub struct AsnLookup {
impl AsnLookup {
pub fn try_from(lookup: geoip2::Isp) -> Result<AsnLookup> {
// parse maxminddb lookup
let asn = match lookup.autonomous_system_number {
Some(asn) => asn,
_ => bail!("autonomous_system_number not set"),
};
let as_org = match lookup.autonomous_system_organization {
Some(org) => org,
_ => bail!("autonomous_system_organization not set"),
};
let asn = lookup.autonomous_system_number
.ok_or_else(|| format_err!("autonomous_system_number not set"))?;
let as_org = lookup.autonomous_system_organization
.ok_or_else(|| format_err!("autonomous_system_organization not set"))?;
// return result
Ok(AsnLookup {
asn,
as_org,
as_org: as_org.to_string(),
})
}
}

View File

@@ -1,9 +1,6 @@
use crate::errors::*;
use exif;
use std::io;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Location {
latitude: f64,

View File

@@ -1,11 +1,8 @@
use crate::engine::structs::LuaMap;
use crate::errors::*;
use kuchiki;
use crate::hlua::AnyLuaValue;
use kuchiki::traits::TendrilSink;
use std::collections::HashMap;
use crate::hlua::AnyLuaValue;
use crate::engine::structs::LuaMap;
#[derive(Debug, PartialEq)]
pub struct Element {

View File

@@ -35,11 +35,7 @@ pub fn decode_stream(x: &str) -> Result<Vec<AnyLuaValue>> {
pub fn lua_array_is_list(array: &[(AnyLuaValue, AnyLuaValue)]) -> bool {
if !array.is_empty() {
let first = &array[0];
if let AnyLuaValue::LuaNumber(_) = first.0 {
true
} else {
false
}
matches!(first.0, AnyLuaValue::LuaNumber(_))
} else {
// true // TODO: this breaks unserialize
false

View File

@@ -46,7 +46,7 @@ impl MqttClient {
stream,
};
let mut pkt = ConnectPacket::new("MQTT", "sn0int");
let mut pkt = ConnectPacket::new("sn0int");
pkt.set_user_name(options.username.clone());
pkt.set_password(options.password.clone());

View File

@@ -10,6 +10,12 @@ pub struct Ratelimiter {
buckets: HashMap<String, Bucket>,
}
impl Default for Ratelimiter {
fn default() -> Self {
Self::new()
}
}
impl Ratelimiter {
pub fn new() -> Ratelimiter {
Ratelimiter {

View File

@@ -91,7 +91,7 @@ impl SocketOptions {
pub enum Stream {
Tcp(TcpStream),
Tls(rustls::StreamOwned<rustls::ClientSession, TcpStream>),
Tls(Box<rustls::StreamOwned<rustls::ClientSession, TcpStream>>),
}
impl Stream {

View File

@@ -107,7 +107,7 @@ fn setup(mut stream: TcpStream, mut session: ClientSession) -> Result<(Stream, T
info!("successfully established tls connection");
let stream = rustls::StreamOwned::new(session, stream);
let stream = Stream::Tls(stream);
let stream = Stream::Tls(Box::new(stream));
Ok((stream, tls))
}

View File

@@ -1,25 +1,24 @@
pub use chrootable_https::{Client, HttpClient, Resolver, Response};
use crate::blobs::{Blob, BlobState};
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use std::ops::Deref;
use crate::engine::structs::LuaMap;
use crate::errors::*;
use crate::json::LuaJsonValue;
use crate::hlua::AnyLuaValue;
use serde_json;
use chrootable_https::{Request, Body, Uri};
pub use chrootable_https::{Client, HttpClient, Resolver, Response};
use chrootable_https::http::HttpTryFrom;
use chrootable_https::http::uri::Parts;
use chrootable_https::http::request::Builder;
use rand::{Rng, thread_rng};
use rand::distributions::Alphanumeric;
use std::fmt;
use std::net::SocketAddr;
use std::sync::Arc;
use serde::Serialize;
use crate::engine::structs::LuaMap;
use crate::json::LuaJsonValue;
use chrootable_https::http::uri::Parts;
use chrootable_https::{Request, Body, Uri};
use serde_urlencoded;
use base64;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::iter;
use std::net::SocketAddr;
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use url::Url;
pub fn url_set_qs<S: Serialize + fmt::Debug>(url: Uri, query: &S) -> Result<Uri> {
let mut parts = Parts::from(url);
@@ -51,7 +50,12 @@ pub struct HttpSession {
impl HttpSession {
pub fn new() -> (String, HttpSession) {
let id: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect();
let mut rng = thread_rng();
let id: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(16)
.collect();
(id.clone(), HttpSession {
id,
cookies: CookieJar::default(),
@@ -67,6 +71,8 @@ pub struct RequestOptions {
user_agent: Option<String>,
json: Option<serde_json::Value>,
form: Option<serde_json::Value>,
#[serde(default)]
follow_redirects: usize,
body: Option<String>,
timeout: Option<u64>,
#[serde(default)]
@@ -84,10 +90,6 @@ impl RequestOptions {
}
}
pub fn default_user_agent() -> String {
format!("sn0int/{}", env!("CARGO_PKG_VERSION"))
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct HttpRequest {
// reference to the HttpSession
@@ -98,7 +100,8 @@ pub struct HttpRequest {
query: Option<HashMap<String, String>>,
headers: Option<HashMap<String, String>>,
basic_auth: Option<(String, String)>,
user_agent: Option<String>,
user_agent: String,
follow_redirects: usize,
body: Option<ReqBody>,
timeout: Option<Duration>,
into_blob: bool,
@@ -107,10 +110,11 @@ pub struct HttpRequest {
}
impl HttpRequest {
pub fn new(session: &HttpSession, method: String, url: String, options: RequestOptions) -> HttpRequest {
pub fn new(session: &HttpSession, method: String, url: String, options: RequestOptions, default_agent: fn() -> String) -> HttpRequest {
let cookies = session.cookies.clone();
let user_agent = options.user_agent.or_else(|| Some(default_user_agent()));
let user_agent = options.user_agent.unwrap_or_else(default_agent);
let timeout = options.timeout.map(Duration::from_millis);
let mut request = HttpRequest {
@@ -122,6 +126,7 @@ impl HttpRequest {
headers: options.headers,
basic_auth: options.basic_auth,
user_agent,
follow_redirects: options.follow_redirects,
body: None,
timeout,
into_blob: options.into_blob,
@@ -144,7 +149,7 @@ impl HttpRequest {
request
}
pub fn send(&self, state: &dyn WebState) -> Result<Response> {
pub fn send(&mut self, state: &dyn WebState) -> Result<Response> {
let mut url = self.url.parse::<Uri>()?;
// set query string
@@ -153,37 +158,9 @@ impl HttpRequest {
}
// start setting up request
let mut req = Request::builder();
req.method(self.method.as_str());
req.uri(url);
let mut observed_headers = HashSet::new();
// set cookies
{
use chrootable_https::header::COOKIE;
let mut cookies = String::new();
for (key, value) in self.cookies.iter() {
if !cookies.is_empty() {
cookies += "; ";
}
cookies.push_str(&format!("{}={}", key, value));
}
if !cookies.is_empty() {
req.header(COOKIE, cookies.as_str());
observed_headers.insert(COOKIE.as_str().to_lowercase());
}
}
let mut req = self.mkrequest(self.method.as_str(), &url);
// add headers
if let Some(ref agent) = self.user_agent {
use chrootable_https::header::USER_AGENT;
req.header(USER_AGENT, agent.as_str());
observed_headers.insert(USER_AGENT.as_str().to_lowercase());
}
if let Some(ref auth) = self.basic_auth {
use chrootable_https::header::AUTHORIZATION;
let &(ref user, ref password) = auth;
@@ -191,9 +168,9 @@ impl HttpRequest {
let auth = base64::encode(&format!("{}:{}", user, password));
let auth = format!("Basic {}", auth);
req.header(AUTHORIZATION, auth.as_str());
observed_headers.insert(AUTHORIZATION.as_str().to_lowercase());
}
let mut observed_headers = HashSet::new();
if let Some(ref headers) = self.headers {
for (k, v) in headers {
req.header(k.as_str(), v.as_str());
@@ -220,24 +197,71 @@ impl HttpRequest {
},
None => Body::empty(),
};
let req = req.body(body)?;
let mut req = req.body(body)?;
debug!("Getting http client");
let client = state.http(&self.proxy)?;
// send request
debug!("Sending http request: {:?}", req);
let res = client.request(req)
.with_timeout(self.timeout)
.wait_for_response()?;
let res = loop {
// send request
debug!("Sending http request: {:?}", req);
let res = client.request(req)
.with_timeout(self.timeout)
.wait_for_response()?;
for cookie in &res.cookies {
HttpRequest::register_cookies_on_state(&self.session, state, cookie);
}
for cookie in &res.cookies {
HttpRequest::register_cookies_on_state(&self.session, state, cookie);
}
if self.follow_redirects > 0 && res.status >= 300 && res.status < 400 {
if let Some(location) = res.headers.get("location") {
let base = Url::parse(&url.to_string())?;
let joined = base.join(&location)?;
url = joined.to_string().parse()?;
req = self.mkrequest("GET", &url).body(Body::empty())?;
self.follow_redirects -= 1;
continue;
}
}
break res;
};
Ok(res)
}
/// create a basic request, reusable when following redirects
fn mkrequest<T>(&self, method: &str, url: T) -> Builder
where Uri: HttpTryFrom<T>,
{
let mut req = Request::builder();
req.method(method);
req.uri(url);
self.attach_cookies(&mut req);
use chrootable_https::header::USER_AGENT;
req.header(USER_AGENT, self.user_agent.as_str());
req
}
fn attach_cookies(&self, req: &mut Builder) {
use chrootable_https::header::COOKIE;
let mut cookies = String::new();
for (key, value) in self.cookies.iter() {
if !cookies.is_empty() {
cookies += "; ";
}
cookies.push_str(&format!("{}={}", key, value));
}
if !cookies.is_empty() {
req.header(COOKIE, cookies.as_str());
}
}
pub fn response_to_lua<S>(&self, state: &S, res: Response) -> Result<LuaMap>
where S: WebState + BlobState
{

View File

@@ -13,6 +13,12 @@ pub struct XmlDocument {
pub children: Vec<XmlElement>,
}
impl Default for XmlDocument {
fn default() -> Self {
Self::new()
}
}
impl XmlDocument {
#[inline(always)]
pub fn new() -> XmlDocument {

View File

@@ -1,23 +1,21 @@
use crate::errors::*;
use std::fmt;
use crate::web;
use crate::config::Config;
use crate::utils;
use chrootable_https::{self, HttpClient, Body, Request, Uri};
use chrootable_https::http::request::Builder as RequestBuilder;
use chrootable_https::header::CONTENT_TYPE;
use rand::{Rng, thread_rng};
use rand::distributions::Alphanumeric;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use serde_json;
use sn0int_common::api::*;
use sn0int_common::{ModuleID, ApiResponse};
use crate::web;
use std::fmt;
pub struct Client {
server: String,
client: chrootable_https::Client<chrootable_https::Resolver>,
session: Option<String>,
os_version: String,
}
impl Client {
@@ -28,10 +26,16 @@ impl Client {
chrootable_https::Client::with_system_resolver_v4()?
};
let os_version = match os_version::detect() {
Ok(v) => v.to_string(),
Err(_) => String::from("unknown"),
};
Ok(Client {
server: config.core.registry.clone(),
client,
session: None,
os_version,
})
}
@@ -39,15 +43,17 @@ impl Client {
self.session = Some(session.into());
}
#[inline]
pub fn random_session() -> String {
thread_rng().sample_iter(&Alphanumeric).take(32).collect()
utils::random_string(32)
}
#[inline]
fn user_agent() -> String {
format!("{}, {}",
web::default_user_agent(),
embedded_triple::get()
fn user_agent(&self) -> String {
format!("sn0int/{}, {}, {}",
env!("CARGO_PKG_VERSION"),
embedded_triple::get(),
self.os_version,
)
}
@@ -56,7 +62,7 @@ impl Client {
info!("Adding session token to request");
request.header("Auth", session.as_str());
}
request.header("User-Agent", Self::user_agent());
request.header("User-Agent", self.user_agent());
let request = request.body(body)?;

View File

@@ -18,10 +18,7 @@ pub struct Args {
impl Args {
pub fn is_sandbox(&self) -> bool {
match self.subcommand {
Some(SubCommand::Sandbox(_)) => true,
_ => false,
}
matches!(self.subcommand, Some(SubCommand::Sandbox(_)))
}
}
@@ -72,6 +69,12 @@ pub enum SubCommand {
/// Manage workspaces
#[structopt(name="workspace")]
Workspace(cmd::workspace_cmd::Args),
/// Calendar
#[structopt(name="cal")]
Cal(cmd::cal_cmd::Args),
/// Notify
#[structopt(name="notify")]
Notify(cmd::notify_cmd::Args),
/// Verify blob storage for corrupt and dangling blobs
#[structopt(name="fsck")]
Fsck(cmd::fsck_cmd::Args),

View File

@@ -1,5 +1,4 @@
use crate::errors::*;
use opener;
use std::fs;
use std::path::PathBuf;
use std::thread;

View File

@@ -70,7 +70,7 @@ impl AutoRule<str> for IpRule {
impl AutoRule<net::IpAddr> for IpRule {
fn matches(&self, ipaddr: &net::IpAddr) -> Result<bool> {
Ok(self.network.contains(ipaddr.clone()))
Ok(self.network.contains(*ipaddr))
}
}

View File

@@ -1,6 +1,5 @@
use crate::db::DatabaseSock;
use crate::errors::*;
use diesel;
use diesel::prelude::*;
use crate::schema::*;
use crate::models::*;

397
src/cal/date.rs Normal file
View File

@@ -0,0 +1,397 @@
use crate::errors::*;
use chrono::Utc;
use chrono::prelude::*;
use crate::cal::{ActivityGrade, DateArg};
use crate::models::*;
use std::collections::HashMap;
use std::collections::VecDeque;
const MONTH_LINES: i32 = 7;
fn merge_months(ctx: &DateContext, months: &[DateSpec]) -> String {
let mut months = months.iter()
.map(|ds| {
let month = ds.to_term_string(ctx);
month.lines()
.map(String::from)
.collect::<VecDeque<_>>()
})
.collect::<Vec<_>>();
let mut out = String::new();
for i in 0..=MONTH_LINES {
let mut first = true;
for m in &mut months {
if !first {
out.push_str(" ");
}
if let Some(line) = m.pop_front() {
out.push_str(&line);
} else {
out.push_str(&" ".repeat(21));
}
first = false;
}
if i < MONTH_LINES {
out.push('\n');
}
}
out
}
fn chunk_months(ctx: &DateContext, months: &[DateSpec]) -> String {
months
.chunks(3)
.map(|m| merge_months(ctx, m))
.fold(String::new(), |a, b| {
if a.is_empty() {
a + &b
} else {
a + "\n" + &b
}
})
}
fn days_in_month(year: i32, month: u32) -> i64 {
let start = Utc.ymd(year, month, 1);
let end = if month == 12 {
Utc.ymd(year + 1, 1, 1)
} else {
Utc.ymd(year, month + 1, 1)
};
end.signed_duration_since(start).num_days()
}
fn setup_graph_map(events: &[Activity]) -> (HashMap<NaiveDate, u64>, u64) {
debug!("Found {} events in selected range", events.len());
let mut cur = None;
let mut ctr = 0;
let mut max = 0;
let mut map = HashMap::new();
for event in events {
let date = event.time.date();
if let Some(cur) = cur.as_mut() {
if date == *cur {
ctr += 1;
} else {
if ctr > max {
max = ctr;
}
map.insert(*cur, ctr);
*cur = date;
ctr = 1;
}
} else {
cur = Some(date);
ctr = 1;
}
}
if ctr > 0 {
if let Some(cur) = cur.take() {
if ctr > max {
max = ctr;
}
map.insert(cur, ctr);
}
}
debug!("Maximum events per day is {}", max);
(map, max)
}
pub struct DateContext {
events: HashMap<NaiveDate, u64>,
max: u64,
today: NaiveDate,
}
impl DateContext {
pub fn new(events: &[Activity], today: NaiveDate) -> DateContext {
let (events, max) = setup_graph_map(&events);
DateContext {
events,
max,
today,
}
}
#[inline]
fn is_today(&self, date: &NaiveDate) -> bool {
self.today == *date
}
#[inline]
fn is_future(&self, date: &NaiveDate) -> bool {
self.today < *date
}
fn activity_for_day(&self, date: &NaiveDate) -> ActivityGrade {
if let Some(events) = self.events.get(date) {
ActivityGrade::from_ratio(*events, self.max)
} else {
ActivityGrade::None
}
}
}
pub enum DateSpec {
Year(i32),
YearMonth((i32, u32)),
YearMonthContext((i32, u32, u32)),
}
impl DateSpec {
pub fn from_args(args: &[DateArg], context: Option<u32>) -> Result<DateSpec> {
if args.len() > 2 {
bail!("Too many datespec args");
}
let today = Utc::today();
let ds = match (args.get(0), args.get(1), context) {
(None, _, None) => DateSpec::YearMonth((today.year(), today.month())),
(None, _, Some(context)) => DateSpec::YearMonthContext((today.year(), today.month(), context)),
(Some(DateArg::Month(month)), None, None) => DateSpec::YearMonth((today.year(), *month)),
(Some(DateArg::Num(year)), None, None) => DateSpec::Year(*year),
(Some(DateArg::Month(month)), Some(DateArg::Num(year)), None) => DateSpec::YearMonth((*year, *month)),
(Some(DateArg::Num(year)), Some(DateArg::Month(month)), None) => DateSpec::YearMonth((*year, *month)),
(Some(DateArg::Month(month)), None, Some(context)) => DateSpec::YearMonthContext((today.year(), *month, context)),
(Some(DateArg::Month(month)), Some(DateArg::Num(year)), Some(context)) => DateSpec::YearMonthContext((*year, *month, context)),
(Some(DateArg::Num(year)), Some(DateArg::Month(month)), Some(context)) => DateSpec::YearMonthContext((*year, *month, context)),
_ => bail!("Combination of datespec args is invalid"),
};
Ok(ds)
}
pub fn start(&self) -> NaiveDate {
match self {
DateSpec::Year(year) => NaiveDate::from_ymd(*year, 1, 1),
DateSpec::YearMonth((year, month)) => NaiveDate::from_ymd(*year, *month, 1),
DateSpec::YearMonthContext((year, month, context)) => {
let mut year = *year - (*context / 12) as i32;
let context = context % 12;
let month = if context >= *month {
year -= 1;
12 - context + month
} else {
month - context
};
NaiveDate::from_ymd(year, month, 1)
},
}
}
pub fn end(&self) -> NaiveDate {
match self {
DateSpec::Year(year) => NaiveDate::from_ymd(year + 1, 1, 1),
DateSpec::YearMonth((year, month)) => {
let (year, month) = if *month == 12 {
(*year + 1, 1)
} else {
(*year, *month + 1)
};
NaiveDate::from_ymd(year, month, 1)
},
DateSpec::YearMonthContext((year, month, _context)) => {
let (year, month) = if *month == 12 {
(*year + 1, 1)
} else {
(*year, *month + 1)
};
NaiveDate::from_ymd(year, month, 1)
},
}
}
pub fn to_term_string(&self, ctx: &DateContext) -> String {
match self {
DateSpec::Year(year) => {
let months = (1..=12)
.map(|month| DateSpec::YearMonth((*year, month)))
.collect::<Vec<_>>();
chunk_months(ctx, &months)
},
DateSpec::YearMonth((year, month)) => {
let mut w = String::new();
let start = Utc.ymd(*year, *month, 1);
let days = days_in_month(*year, *month) as u32;
w.push_str(&format!("{:^21}\n", start.format("%B %Y")));
w.push_str(" Su Mo Tu We Th Fr Sa\n");
let mut cur_week_day = start.weekday();
let week_progress = cur_week_day.num_days_from_sunday() as usize;
w.push_str(&" ".repeat(week_progress));
let mut week_written = week_progress * 3;
for cur_day in 1..=days {
let date = NaiveDate::from_ymd(*year, *month, cur_day);
if !ctx.is_future(&date) {
let activity = ctx.activity_for_day(&date);
w.push_str(activity.as_term_str());
}
if ctx.is_today(&date) {
w.push_str("\x1b[1m#");
} else {
w.push(' ');
}
w.push_str(&format!("{:2}", cur_day));
week_written += 3;
w.push_str("\x1b[0m");
// detect end of the week
if cur_week_day == Weekday::Sat {
if cur_day != days {
w.push('\n');
}
week_written = 0;
}
cur_week_day = cur_week_day.succ();
}
if week_written != 0 {
w.push_str(&" ".repeat(21 - week_written));
}
w
}
DateSpec::YearMonthContext((_year, _month, context)) => {
let start = self.start();
let mut year = start.year();
let mut month = start.month();
let mut months = Vec::new();
for _ in 0..=*context {
months.push(DateSpec::YearMonth((year, month)));
if month == 12 {
year += 1;
month = 1;
} else {
month += 1;
}
}
chunk_months(ctx, &months)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context() -> DateContext {
DateContext {
events: HashMap::new(),
max: 0,
today: NaiveDate::from_ymd(2020, 05, 30),
}
}
#[test]
fn test_days_in_month_2020_05() {
let days = days_in_month(2020, 05);
assert_eq!(days, 31);
}
#[test]
fn test_days_in_month_2020_04() {
let days = days_in_month(2020, 04);
assert_eq!(days, 30);
}
#[test]
fn test_days_in_month_2020_02() {
let days = days_in_month(2020, 02);
assert_eq!(days, 29);
}
#[test]
fn small_max_activity_0() {
let events = HashMap::new();
let ctx = DateContext {
events,
max: 0,
today: NaiveDate::from_ymd(2020, 06, 06),
};
let grade = ctx.activity_for_day(&NaiveDate::from_ymd(2020, 06, 06));
assert_eq!(grade, ActivityGrade::None);
}
#[test]
fn test_datespec_year_month() {
let ds = DateSpec::YearMonth((2020, 05));
let out = ds.to_term_string(&context());
assert_eq!(out, " May 2020
Su Mo Tu We Th Fr Sa
\u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m\u{1b}[1m#30\u{1b}[0m
31\u{1b}[0m ");
}
#[test]
fn test_datespec_year_month_ends_on_sat() {
let ds = DateSpec::YearMonth((2020, 10));
let out = ds.to_term_string(&context());
assert_eq!(out, " October 2020
Su Mo Tu We Th Fr Sa
1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m
4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m
11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m
18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m
25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m");
}
#[test]
fn test_datespec_year() {
let ds = DateSpec::Year(2020);
let out = ds.to_term_string(&context());
assert_eq!(out, " January 2020 February 2020 March 2020
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
\u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 30\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 31\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 30\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 31\u{1b}[0m
April 2020 May 2020 June 2020
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
\u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m
\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 30\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m\u{1b}[1m#30\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m
31\u{1b}[0m
July 2020 August 2020 September 2020
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 1\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m
5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m
12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m
19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m
26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m
30\u{1b}[0m 31\u{1b}[0m
October 2020 November 2020 December 2020
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m
4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m
11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m
18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m
25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m
");
}
}

166
src/cal/mod.rs Normal file
View File

@@ -0,0 +1,166 @@
use crate::errors::*;
use std::str::FromStr;
pub mod date;
pub mod time;
#[derive(Debug)]
pub enum DateArg {
Month(u32),
Num(i32),
}
impl FromStr for DateArg {
type Err = Error;
fn from_str(s: &str) -> Result<DateArg> {
let ds = match s.to_lowercase().as_str() {
"jan" | "january" => DateArg::Month(1),
"feb" | "february" => DateArg::Month(2),
"mar" | "march" => DateArg::Month(3),
"apr" | "april" => DateArg::Month(4),
"may" => DateArg::Month(5),
"jun" | "june" => DateArg::Month(6),
"jul" | "july" => DateArg::Month(7),
"aug" | "august" => DateArg::Month(8),
"sep" | "september" => DateArg::Month(9),
"oct" | "october" => DateArg::Month(10),
"nov" | "november" => DateArg::Month(11),
"dec" | "december" => DateArg::Month(12),
_ => {
let num = s.parse::<i32>()
.context("Input is not a month and not a number")?;
DateArg::Num(num)
},
};
Ok(ds)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ActivityGrade {
None,
One,
Two,
Three,
Four,
}
impl ActivityGrade {
fn as_term_str(&self) -> &'static str {
match self {
ActivityGrade::None => "\x1b[97m\x1b[48;5;238m",
ActivityGrade::One => "\x1b[30m\x1b[48;5;148m",
ActivityGrade::Two => "\x1b[30m\x1b[48;5;71m",
ActivityGrade::Three => "\x1b[97m\x1b[48;5;34m",
ActivityGrade::Four => "\x1b[97m\x1b[48;5;22m",
}
}
pub fn from_ratio(num: u64, max: u64) -> ActivityGrade {
let max = max as f64;
let num = num as f64;
let step = max / 4.0;
let x = num / step;
if x <= 1.0 {
ActivityGrade::One
} else if x <= 2.0 {
ActivityGrade::Two
} else if x <= 3.0 {
ActivityGrade::Three
} else {
ActivityGrade::Four
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn small_max_activity_1() {
let grade = ActivityGrade::from_ratio(1, 1);
assert_eq!(grade, ActivityGrade::Four);
}
#[test]
fn small_max_activity_2() {
let grade = ActivityGrade::from_ratio(2, 2);
assert_eq!(grade, ActivityGrade::Four);
}
#[test]
fn small_max_activity_3() {
let grade = ActivityGrade::from_ratio(3, 3);
assert_eq!(grade, ActivityGrade::Four);
}
#[test]
fn small_max_activity_4() {
let grade = ActivityGrade::from_ratio(4, 4);
assert_eq!(grade, ActivityGrade::Four);
}
#[test]
fn small_max_activity_2_but_is_1() {
let grade = ActivityGrade::from_ratio(1, 2);
assert_eq!(grade, ActivityGrade::Two);
}
#[test]
fn small_max_activity_3_but_is_2() {
let grade = ActivityGrade::from_ratio(2, 3);
assert_eq!(grade, ActivityGrade::Three);
}
#[test]
fn small_max_activity_3_but_is_1() {
let grade = ActivityGrade::from_ratio(1, 3);
assert_eq!(grade, ActivityGrade::Two);
}
#[test]
fn small_max_activity_4_but_is_3() {
let grade = ActivityGrade::from_ratio(3, 4);
assert_eq!(grade, ActivityGrade::Three);
}
#[test]
fn small_max_activity_4_but_is_2() {
let grade = ActivityGrade::from_ratio(2, 4);
assert_eq!(grade, ActivityGrade::Two);
}
#[test]
fn small_max_activity_4_but_is_1() {
let grade = ActivityGrade::from_ratio(1, 4);
assert_eq!(grade, ActivityGrade::One);
}
#[test]
fn small_max_activity_5_but_is_4() {
let grade = ActivityGrade::from_ratio(4, 5);
assert_eq!(grade, ActivityGrade::Four);
}
#[test]
fn small_max_activity_5_but_is_3() {
let grade = ActivityGrade::from_ratio(3, 5);
assert_eq!(grade, ActivityGrade::Three);
}
#[test]
fn small_max_activity_5_but_is_2() {
let grade = ActivityGrade::from_ratio(2, 5);
assert_eq!(grade, ActivityGrade::Two);
}
#[test]
fn small_max_activity_5_but_is_1() {
let grade = ActivityGrade::from_ratio(1, 5);
assert_eq!(grade, ActivityGrade::One);
}
}

209
src/cal/time.rs Normal file
View File

@@ -0,0 +1,209 @@
use crate::errors::*;
use chrono::Duration;
use chrono::prelude::*;
use crate::cal::{ActivityGrade, DateArg};
use crate::models::*;
use std::collections::HashMap;
const MIN_PER_DAY: u32 = 1440;
fn round_to_slice(time: &NaiveDateTime, slice_duration: u32) -> NaiveDateTime {
let date = time.date();
let hour = time.hour();
let mins = time.minute();
let slice = mins - (mins % slice_duration);
date.and_hms(hour, slice, 0)
}
fn setup_graph_map(events: &[Activity], slice_duration: u32) -> (HashMap<NaiveDateTime, u64>, u64) {
debug!("Found {} events in selected range", events.len());
let mut cur = None;
let mut ctr = 0;
let mut max = 0;
let mut map = HashMap::new();
for event in events {
let time = round_to_slice(&event.time, slice_duration);
if let Some(cur) = cur.as_mut() {
if time == *cur {
ctr += 1;
} else {
if ctr > max {
max = ctr;
}
map.insert(*cur, ctr);
*cur = time;
ctr = 1;
}
} else {
cur = Some(time);
ctr = 1;
}
}
if ctr > 0 {
if let Some(cur) = cur.take() {
if ctr > max {
max = ctr;
}
map.insert(cur, ctr);
}
}
debug!("Maximum events per slice is {}", max);
(map, max)
}
pub struct DateTimeContext {
events: HashMap<NaiveDateTime, u64>,
max: u64,
now: NaiveDateTime,
pub slice_width: u32,
pub slice_duration: u32,
}
impl DateTimeContext {
pub fn new(events: &[Activity], now: NaiveDateTime, slice_width: u32, slice_duration: u32) -> DateTimeContext {
let (events, max) = setup_graph_map(&events, slice_duration);
DateTimeContext {
events,
max,
now: round_to_slice(&now, slice_duration),
slice_width,
slice_duration,
}
}
#[inline]
fn is_future(&self, time: &NaiveDateTime) -> bool {
self.now < *time
}
#[inline]
fn slices_per_hour(&self) -> u32 {
60 / self.slice_duration
}
#[inline]
fn hour_width(&self) -> u32 {
self.slices_per_hour() * self.slice_width
}
fn activity_for_slice(&self, time: &NaiveDateTime) -> ActivityGrade {
if let Some(events) = self.events.get(time) {
ActivityGrade::from_ratio(*events, self.max)
} else {
ActivityGrade::None
}
}
}
pub struct DateTimeSpec {
start: NaiveDate,
end: NaiveDate,
}
impl DateTimeSpec {
pub fn from_args(args: &[DateArg], context: Option<u32>) -> Result<DateTimeSpec> {
let today = Utc::today().naive_utc();
if args.is_empty() {
let mut start = today;
if let Some(context) = context {
start = start.checked_sub_signed(Duration::days(context as i64))
.ok_or_else(|| format_err!("Failed travel back in time"))?;
}
Ok(DateTimeSpec {
start,
end: today,
})
} else {
todo!()
}
}
pub fn start(&self) -> &NaiveDate {
&self.start
}
pub fn end(&self) -> &NaiveDate {
&self.end
}
fn push_date(&self, w: &mut String, date: &NaiveDate) {
let weekday = date.weekday();
let is_weekend = weekday == Weekday::Sat || weekday == Weekday::Sun;
if is_weekend {
w.push_str("\x1b[1m");
}
w.push_str(&date.format("%Y-%m-%d (%a) ").to_string());
if is_weekend {
w.push_str("\x1b[0m");
}
}
pub fn to_term_string(&self, ctx: &DateTimeContext) -> String {
let mut w = String::new();
// add legend
w.push_str(&" ".repeat(11));
for x in 0..24 {
w.push_str(&format!("{:02}", x));
for i in 0..ctx.hour_width() {
if i >= 2 {
w.push(' ');
}
}
}
w.push('\n');
// add days
let mut date = self.start;
while date <= self.end {
self.push_date(&mut w, &date);
let mut hours = 0;
let mut mins = 0;
for _ in 0..(MIN_PER_DAY / ctx.slice_duration) {
let time = date.and_hms(hours, mins, 0);
if !ctx.is_future(&time) {
let activity = ctx.activity_for_slice(&time);
w.push_str(activity.as_term_str());
} else {
w.push_str("\x1b[0m");
}
for _ in 0..ctx.slice_width {
w.push(' ');
}
mins += ctx.slice_duration;
if mins >= 60 {
hours += 1;
mins = 0;
}
}
w.push_str("\x1b[0m");
if date < self.end {
w.push('\n');
}
date = date.checked_add_signed(Duration::days(1))
.expect("Failed to get next day");
}
w
}
}

View File

@@ -2,6 +2,7 @@ use crate::errors::*;
use crate::blobs::Blob;
use crate::cmd::Cmd;
use crate::db::DbChange;
use crate::gfx;
use crate::models::*;
use crate::shell::Shell;
@@ -9,10 +10,11 @@ use structopt::StructOpt;
use structopt::clap::AppSettings;
use crate::utils;
use crate::term;
use std::fmt::Debug;
use std::fs;
use std::io::{self, BufRead};
use std::net;
use std::net::SocketAddr;
use ipnetwork;
use std::path::Path;
use walkdir::WalkDir;
@@ -24,6 +26,9 @@ pub struct Args {
/// Do not actually insert into database
#[structopt(short="n", long="dry-run")]
dry_run: bool,
/// Stream structs from stdin line by line
#[structopt(long)]
stdin: bool,
}
#[derive(Debug, StructOpt)]
@@ -74,25 +79,68 @@ pub enum Target {
impl Cmd for Args {
fn run(self, rl: &mut Shell) -> Result<()> {
match self.subcommand {
Target::Domain(args) => args.insert(rl, self.dry_run),
Target::Subdomain(args) => args.insert(rl, self.dry_run),
Target::IpAddr(args) => args.insert(rl, self.dry_run),
Target::Url(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),
Target::Netblock(args) => args.insert(rl, self.dry_run),
Target::Port(args) => args.insert(rl, self.dry_run),
Target::CryptoAddr(args) => args.insert(rl, self.dry_run),
if self.stdin {
match self.subcommand {
Target::Domain(_) => stream_stdin::<AddDomain>(rl),
Target::Subdomain(_) => stream_stdin::<AddSubdomain>(rl),
Target::IpAddr(_) => stream_stdin::<AddIpAddr>(rl),
Target::Url(_) => stream_stdin::<AddUrl>(rl),
Target::Email(_) => stream_stdin::<AddEmail>(rl),
Target::PhoneNumber(_) => stream_stdin::<AddPhoneNumber>(rl),
Target::Port(_) => stream_stdin::<AddPort>(rl),
Target::CryptoAddr(_) => stream_stdin::<AddCryptoAddr>(rl),
Target::Device(_) | Target::Network(_) | Target::Account(_) | Target::Breach(_) | Target::Image(_) | Target::Netblock(_) => {
bail!("This struct doesn't support streaming from stdin")
},
}
} else {
match self.subcommand {
Target::Domain(args) => args.insert(rl, self.dry_run),
Target::Subdomain(args) => args.insert(rl, self.dry_run),
Target::IpAddr(args) => args.insert(rl, self.dry_run),
Target::Url(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),
Target::Netblock(args) => args.insert(rl, self.dry_run),
Target::Port(args) => args.insert(rl, self.dry_run),
Target::CryptoAddr(args) => args.insert(rl, self.dry_run),
}
}
}
}
fn stream_stdin<T: InsertFromString>(rl: &mut Shell) -> Result<()> {
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
match T::from_string(rl, line) {
Ok(insert) => {
debug!("Received {:?}", insert);
let result = rl.db().insert_generic(insert.clone())?;
if let Some((DbChange::Insert, _)) = result {
if let Ok(value) = insert.value(rl.db()) {
let log = format!("Adding {} {:?}", insert.family(), value);
term::success(&log);
}
}
},
Err(err) => term::error(&format!("Error: {:#}", err)),
}
}
Ok(())
}
trait InsertFromString {
// TODO: some implementations run insert_struct instead of just transforming
fn from_string(rl: &mut Shell, x: String) -> Result<Insert>;
}
trait IntoInsert: Sized {
fn into_insert(self, rl: &mut Shell) -> Result<Insert>;
@@ -116,7 +164,12 @@ impl IntoInsert for AddDomain {
Some(domain) => domain,
_ => utils::question("Domain")?,
};
Self::from_string(rl, domain)
}
}
impl InsertFromString for AddDomain {
fn from_string(rl: &mut Shell, domain: String) -> Result<Insert> {
// ensure input is a valid domain
let dns_name = rl.psl()?.parse_dns_name(&domain)
.map_err(|e| format_err!("Failed to parse domain: {}", e))?;
@@ -143,7 +196,12 @@ impl IntoInsert for AddSubdomain {
Some(subdomain) => subdomain,
_ => utils::question("Subdomain")?,
};
Self::from_string(rl, subdomain)
}
}
impl InsertFromString for AddSubdomain {
fn from_string(rl: &mut Shell, subdomain: String) -> Result<Insert> {
let dns_name = rl.psl()?.parse_dns_name(&subdomain)
.map_err(|e| format_err!("Failed to parse dns_name: {}", e))?;
@@ -170,7 +228,8 @@ pub struct AddIpAddr {
}
impl IntoInsert for AddIpAddr {
fn into_insert(self, _rl: &mut Shell) -> Result<Insert> {
fn into_insert(self, rl: &mut Shell) -> Result<Insert> {
// TODO: change this to Option<String> so we don't need to parse back and forth
let ipaddr = match self.ipaddr {
Some(ipaddr) => ipaddr,
_ => {
@@ -178,6 +237,13 @@ impl IntoInsert for AddIpAddr {
ipaddr.parse()?
},
};
Self::from_string(rl, ipaddr.to_string())
}
}
impl InsertFromString for AddIpAddr {
fn from_string(_rl: &mut Shell, ipaddr: String) -> Result<Insert> {
let ipaddr = ipaddr.parse()?;
let family = match ipaddr {
net::IpAddr::V4(_) => "4",
@@ -214,7 +280,12 @@ impl IntoInsert for AddUrl {
Some(url) => url,
_ => utils::question("URL")?,
};
Self::from_string(rl, url)
}
}
impl InsertFromString for AddUrl {
fn from_string(rl: &mut Shell, url: String) -> Result<Insert> {
let parts = url::Url::parse(&url)?;
let subdomain = parts.domain()
.ok_or_else(|| format_err!("url doesn't have a domain host"))?;
@@ -258,12 +329,18 @@ pub struct AddEmail {
}
impl IntoInsert for AddEmail {
fn into_insert(self, _rl: &mut Shell) -> Result<Insert> {
fn into_insert(self, rl: &mut Shell) -> Result<Insert> {
let email = match self.email {
Some(email) => email,
_ => utils::question("Email")?,
};
Self::from_string(rl, email)
}
}
impl InsertFromString for AddEmail {
fn from_string(_rl: &mut Shell, email: String) -> Result<Insert> {
// TODO: consider doing basic validation
Ok(Insert::Email(NewEmail {
value: email,
displayname: None,
@@ -290,9 +367,30 @@ impl IntoInsert for AddPhoneNumber {
},
};
// TODO: consider doing basic validation
Ok(Insert::PhoneNumber(NewPhoneNumber {
value: phonenumber,
name: 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,
unscoped: false,
}))
}
}
impl InsertFromString for AddPhoneNumber {
fn from_string(_rl: &mut Shell, phonenumber: String) -> Result<Insert> {
// TODO: consider doing basic validation
Ok(Insert::PhoneNumber(NewPhoneNumber {
value: phonenumber,
name: None,
valid: None,
last_online: None,
country: None,
@@ -328,7 +426,7 @@ impl IntoInsert for AddDevice {
Ok(Insert::Device(NewDevice {
value: mac,
name: name,
name,
hostname: None,
vendor: None,
last_seen: None,
@@ -393,9 +491,9 @@ impl IntoInsert for AddAccount {
let value = format!("{}/{}", service, username);
Ok(Insert::Account(NewAccount {
value: value,
service: service,
username: username,
value,
service,
username,
displayname: None,
email: None,
url: None,
@@ -417,10 +515,7 @@ impl IntoInsert for AddBreach {
fn into_insert(self, _rl: &mut Shell) -> Result<Insert> {
let name = match self.name {
Some(name) => name,
_ => {
let name = utils::question("Name")?;
name
}
_ => utils::question("Name")?,
};
Ok(Insert::Breach(NewBreach {
@@ -453,7 +548,7 @@ impl IntoInsert for AddImage {
let path = match path {
Ok(path) => path,
Err(err) => {
let path = err.path().unwrap_or(Path::new("")).display();
let path = err.path().unwrap_or_else(|| Path::new("")).display();
let err = err.io_error()
.map(|err| err.to_string())
@@ -575,12 +670,24 @@ impl IntoInsert for AddPort {
};
let addr = if let Some(addr) = self.addr {
// TODO: change this to a regular string so we don't parse->fmt->parse
addr
} else {
let addr = utils::question("IP:Port")?;
addr.parse()?
};
Self::from_string(rl, format!("{}/{}", protocol, addr))
}
}
impl InsertFromString for AddPort {
fn from_string(rl: &mut Shell, value: String) -> Result<Insert> {
let idx = value.find('/')
.ok_or_else(|| format_err!("Could not locate `/` delimiter for protocol/ip:port"))?;
let protocol = value[..idx].to_string();
let addr = value[idx+1..].parse::<SocketAddr>()?;
let family = match addr.ip() {
net::IpAddr::V4(_) => "4",
net::IpAddr::V6(_) => "6",
@@ -606,8 +713,6 @@ impl IntoInsert for AddPort {
_ => bail!("IpAddr is out out of scope"),
};
let value = format!("{}/{}", protocol, addr);
Ok(Insert::Port(NewPort {
ip_addr_id,
value,
@@ -631,13 +736,18 @@ pub struct AddCryptoAddr {
}
impl IntoInsert for AddCryptoAddr {
fn into_insert(self, _rl: &mut Shell) -> Result<Insert> {
fn into_insert(self, rl: &mut Shell) -> Result<Insert> {
let address = if let Some(address) = self.address {
address
} else {
utils::question("Address")?
};
Self::from_string(rl, address)
}
}
impl InsertFromString for AddCryptoAddr {
fn from_string(_rl: &mut Shell, address: String) -> Result<Insert> {
Ok(Insert::CryptoAddr(NewCryptoAddr {
value: address,
currency: None,

66
src/cmd/cal_cmd.rs Normal file
View File

@@ -0,0 +1,66 @@
use crate::errors::*;
use chrono::Utc;
use crate::cal::DateArg;
use crate::cal::date::{DateContext, DateSpec};
use crate::cal::time::{DateTimeContext, DateTimeSpec};
use crate::cmd::Cmd;
use crate::models::*;
use crate::shell::Shell;
use structopt::StructOpt;
use structopt::clap::AppSettings;
#[derive(Debug, StructOpt)]
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
pub struct Args {
/// Show additional months for context
#[structopt(short="C", long)]
context: Option<u32>,
/// Group events in 12 min slices
#[structopt(short="T", long, group = "view")]
time: bool,
/// Group events by hour
#[structopt(short="H", long, group = "view")]
hourly: bool,
args: Vec<DateArg>,
}
impl Cmd for Args {
#[inline]
fn run(self, rl: &mut Shell) -> Result<()> {
if self.time || self.hourly {
let dts = DateTimeSpec::from_args(&self.args, self.context)
.context("Failed to parse date spec")?;
let filter = ActivityFilter {
topic: None,
since: Some(dts.start().and_hms(0, 0, 0)),
until: Some(dts.end().and_hms(23, 59, 59)),
location: false,
};
let events = Activity::query(rl.db(), &filter)?;
let (slice_width, slice_duration) = if self.hourly {
(3, 60)
} else {
(1, 12)
};
let ctx = DateTimeContext::new(&events, Utc::now().naive_utc(), slice_width, slice_duration);
println!("{}", dts.to_term_string(&ctx));
} else {
let ds = DateSpec::from_args(&self.args, self.context)
.context("Failed to parse date spec")?;
let filter = ActivityFilter {
topic: None,
since: Some(ds.start().and_hms(0, 0, 0)),
until: Some(ds.end().and_hms(23, 59, 59)),
location: false,
};
let events = Activity::query(rl.db(), &filter)?;
let ctx = DateContext::new(&events, Utc::today().naive_utc());
println!("{}", ds.to_term_string(&ctx));
}
Ok(())
}
}

View File

@@ -3,14 +3,13 @@ use crate::errors::*;
use crate::blobs::Blob;
use crate::cmd::Cmd;
use crate::db::ttl;
use crate::models::*;
use crate::shell::Shell;
use serde_json;
use serde::Serialize;
use std::io::{self, Write};
use structopt::StructOpt;
use structopt::clap::AppSettings;
use strum_macros::{EnumString, IntoStaticStr};
use crate::models::*;
#[derive(Debug, StructOpt)]
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
@@ -22,7 +21,7 @@ pub struct Args {
impl Cmd for Args {
fn run(self, rl: &mut Shell) -> Result<()> {
ttl::reap_expired(rl.db())?;
ttl::reap_expired(rl)?;
match self.format {
Format::Json => export::<JsonFormat>(rl),
Format::JsonBlobs => export::<JsonBlobsFormat>(rl),

View File

@@ -20,6 +20,7 @@ pub mod activity_cmd;
pub mod add_cmd;
pub mod autonoscope_cmd;
pub mod autoscope_cmd;
pub mod cal_cmd;
pub mod delete_cmd;
pub mod export_cmd;
pub mod fsck_cmd;
@@ -29,6 +30,7 @@ pub mod use_cmd;
pub mod select_cmd;
pub mod keyring_cmd;
pub mod noscope_cmd;
pub mod notify_cmd;
pub mod pkg_cmd;
pub mod set_cmd;
pub mod scope_cmd;

99
src/cmd/notify_cmd.rs Normal file
View File

@@ -0,0 +1,99 @@
use crate::errors::*;
use crate::cmd::Cmd;
use crate::engine::Module;
// use crate::models::*;
use crate::notify::{self, Notification};
use crate::options::{self, Opt};
use crate::shell::Shell;
use crate::term;
use sn0int_std::ratelimits::Ratelimiter;
use structopt::StructOpt;
use structopt::clap::AppSettings;
#[derive(Debug, StructOpt)]
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
pub struct Args {
#[structopt(subcommand)]
subcommand: Subcommand,
}
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Manually add a notification to the outbox
Send(SendArgs),
/// Show the current outbox
Outbox,
/// Execute a module directly instead of sending a message
Exec(ExecArgs),
/// Try to deliver all messages in our outbox
Deliver,
}
#[derive(Debug, StructOpt)]
pub struct SendArgs {
/// Evaluate the routing rules, but do not actually send a notification
#[structopt(short="n", long)]
pub dry_run: bool,
pub topic: String,
#[structopt(flatten)]
pub notification: Notification,
}
#[derive(Debug, StructOpt)]
pub struct ExecArgs {
pub module: String,
#[structopt(short="o", long="option")]
pub options: Vec<options::Opt>,
#[structopt(short="v", long="verbose", parse(from_occurrences))]
verbose: u64,
#[structopt(flatten)]
pub notification: Notification,
}
fn print_summary(module: &Module, sent: usize, errors: usize) {
let mut out = if sent == 1 {
String::from("Sent 1 notification")
} else {
format!("Sent {} notifications", sent)
};
out.push_str(&format!(" with {}", module.canonical()));
if errors > 0 {
out.push_str(&format!(" ({} errors)", errors));
}
term::info(&out);
}
fn send(args: SendArgs, rl: &mut Shell) -> Result<()> {
rl.signal_register().catch_ctrl();
notify::run_router(rl, &mut term::Term, &mut Ratelimiter::new(), args.dry_run, &args.topic, &args.notification)?;
rl.signal_register().reset_ctrlc();
Ok(())
}
fn exec(args: ExecArgs, rl: &mut Shell) -> Result<()> {
let module = rl.library().get(&args.module)?.clone();
let options = Opt::collect(&args.options);
rl.signal_register().catch_ctrl();
let errors = notify::exec(rl, &module, &mut Ratelimiter::new(), options, args.verbose, &args.notification)?;
rl.signal_register().reset_ctrlc();
print_summary(&module, 1, errors);
Ok(())
}
impl Cmd for Args {
#[inline]
fn run(self, rl: &mut Shell) -> Result<()> {
match self.subcommand {
Subcommand::Send(args) => send(args, rl),
Subcommand::Outbox => todo!(),
Subcommand::Exec(args) => exec(args, rl),
Subcommand::Deliver => todo!(),
}
}
}

View File

@@ -5,13 +5,15 @@ use crate::api::Client;
use crate::args;
use crate::config::Config;
use crate::cmd::{Cmd, LiteCmd};
use crate::engine::Library;
use crate::engine::{Library, Module};
use crate::registry::{self, InstallTask, UpdateTask, Updater};
use crate::shell::Shell;
use crate::update::AutoUpdater;
use crate::worker;
use colored::Colorize;
use colored::{Color, Colorize};
use sn0int_common::ModuleID;
use sn0int_common::metadata::Stealth;
use std::collections::HashSet;
use std::fmt::Write;
use std::sync::Arc;
use structopt::StructOpt;
@@ -70,7 +72,10 @@ pub struct List {
pub source: Option<String>,
/// List outdated modules
#[structopt(long="outdated")]
pub outdated: bool,
pub outdated_only: bool,
/// Only show modules with equal or better stealth level
#[structopt(long="stealth", possible_values=Stealth::variants())]
pub stealth: Option<Stealth>,
/// Filter by pattern
#[structopt(default_value="*")]
pub pattern: String,
@@ -95,6 +100,34 @@ enum ModuleReload {
No,
}
#[inline]
fn write_tag(out: &mut String, color: Color, txt: &str) -> Result<()> {
write!(out, " [{}]", txt.color(color))?;
Ok(())
}
fn print_module(module: &Module, is_outdated: bool) -> Result<()> {
let mut out = String::new();
write!(&mut out, "{}/{} {}", module.author().purple(),
module.name(),
module.version().blue())?;
match module.stealth() {
Stealth::Loud => write_tag(&mut out, Color::Yellow, "LOUD")?,
Stealth::Normal => (),
Stealth::Passive => write_tag(&mut out, Color::Green, "passive")?,
Stealth::Offline => write_tag(&mut out, Color::Green, "offline")?,
};
if is_outdated {
write_tag(&mut out, Color::Red, "outdated")?;
}
println!("{}", out.bold());
println!(" {}", module.description());
Ok(())
}
fn run_subcommand(subcommand: SubCommand, library: &Library, config: &Config) -> Result<ModuleReload> {
match subcommand {
SubCommand::List(list) => {
@@ -114,16 +147,18 @@ fn run_subcommand(subcommand: SubCommand, library: &Library, config: &Config) ->
continue;
}
let mut out = String::new();
write!(&mut out, "{} ({})", canonical.green(),
module.version().yellow())?;
if autoupdate.is_outdated(&canonical) {
write!(&mut out, " {}", "[outdated]".red())?;
} else if list.outdated {
if let Some(stealth) = &list.stealth {
if !module.stealth().equal_or_better(&stealth) {
continue;
}
}
let is_outdated = autoupdate.is_outdated(&canonical);
if list.outdated_only && !is_outdated {
continue;
}
println!("{}", out);
println!("\t{}", module.description());
print_module(module, is_outdated)?;
}
Ok(ModuleReload::No)
},
@@ -174,17 +209,30 @@ fn run_subcommand(subcommand: SubCommand, library: &Library, config: &Config) ->
let updater = Arc::new(Updater::new(&config)?);
let mut autoupdate = AutoUpdater::load()?;
let installed = library.list()
.into_iter()
.map(|module| module.id())
.collect::<HashSet<_>>();
let modules = client.quickstart()?
.into_iter()
.map(|module| {
InstallTask::new(Install {
module: ModuleID {
author: module.author,
name: module.name,
},
version: None,
force: false,
}, updater.clone())
.filter_map(|module| {
let id = ModuleID {
author: module.author,
name: module.name,
};
if !installed.contains(&id) {
info!("Queueing for install: {}", id);
Some(InstallTask::new(Install {
module: id,
version: None,
force: false,
}, updater.clone()))
} else {
info!("Skipping already installed module: {}", id);
None
}
})
.collect::<Vec<_>>();

View File

@@ -14,8 +14,8 @@ use crate::term;
use crate::utils;
use crate::worker;
use serde::Serialize;
use serde_json;
use sn0int_common::metadata::Source;
use sn0int_std::ratelimits::Ratelimiter;
use std::collections::HashMap;
use structopt::StructOpt;
use structopt::clap::AppSettings;
@@ -92,7 +92,7 @@ fn prepare_args<T: Scopable + Serialize + Model>(rl: &Shell, filter: &Filter, pa
.collect()
}
fn prepare_keyring(keyring: &mut KeyRing, module: &Module, params: &Params) -> Result<()> {
pub fn prepare_keyring(keyring: &mut KeyRing, module: &Module, params: &Params) -> Result<()> {
for namespace in keyring.unauthorized_namespaces(&module) {
let grant_access = if params.deny_keyring {
false
@@ -133,6 +133,7 @@ fn get_args(rl: &mut Shell, module: &Module) -> Result<Vec<(serde_json::Value, O
Some(Source::Ports) => prepare_args::<Port>(rl, &filter, None),
Some(Source::Netblocks) => prepare_args::<Netblock>(rl, &filter, None),
Some(Source::CryptoAddrs(currency)) => prepare_args::<CryptoAddr>(rl, &filter, currency.as_ref()),
Some(Source::Notifications) => bail!("Notification modules can't be executed like this"),
Some(Source::KeyRing(namespace)) => {
let keyring = rl.keyring();
if keyring.is_access_granted(&module, &namespace) {
@@ -160,14 +161,14 @@ pub fn dump_sandbox_init_msg(rl: &mut Shell, params: Params, options: HashMap<St
let keyring = rl.keyring().request_keys(&module);
let dns_config = Resolver::from_system_v4()?;
let proxy = rl.config().network.proxy.clone();
let proxy = rl.config().network.proxy;
let args = get_args(rl, &module)?;
for (arg, _pretty_arg, blobs) in args {
let start_cmd = StartCommand::new(params.verbose,
keyring.clone(),
dns_config.clone(),
proxy.clone(),
proxy,
options.clone(),
module.clone(),
arg,
@@ -188,7 +189,7 @@ pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>)
let args = get_args(rl, &module)?;
rl.signal_register().catch_ctrl();
let errors = worker::spawn(rl, &module, args, &params, rl.config().network.proxy.clone(), options);
let errors = worker::spawn(rl, &module, &mut Ratelimiter::new(), args, &params, rl.config().network.proxy, options);
rl.signal_register().reset_ctrlc();
if errors > 0 {
@@ -206,7 +207,7 @@ pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>)
impl Cmd for Args {
fn run(self, rl: &mut Shell) -> Result<()> {
ttl::reap_expired(rl.db())?;
ttl::reap_expired(rl)?;
let options = match rl.options_mut() {
Some(options) => options.clone(),
_ => HashMap::new(),

View File

@@ -5,7 +5,6 @@ use crate::db::ttl;
use crate::filters::{Target, Filter};
use crate::shell::Shell;
use serde::Serialize;
use serde_json;
use structopt::StructOpt;
use structopt::clap::AppSettings;
use crate::models::*;
@@ -94,7 +93,7 @@ impl<'a, 'b> Printer<'a, 'b> {
impl Cmd for Args {
fn run(self, rl: &mut Shell) -> Result<()> {
ttl::reap_expired(rl.db())?;
ttl::reap_expired(rl)?;
let printer = Printer::new(rl, &self);
match &self.subcommand {

View File

@@ -44,6 +44,7 @@ pub fn run(rl: &mut Shell, args: &[String]) -> Result<()> {
Source::Ports => select::<Port>(rl, None)?,
Source::Netblocks => select::<Netblock>(rl, None)?,
Source::CryptoAddrs(currency) => select::<CryptoAddr>(rl, currency.as_ref())?,
Source::Notifications => bail!("Notifications can't be set as target"),
Source::KeyRing(namespace) => {
for key in rl.keyring().list_for(&namespace) {
println!("{}:{}", key.namespace, key.name);
@@ -79,6 +80,7 @@ fn count_selected(rl: &mut Shell, source: &Source) -> Result<usize> {
Source::Ports => db.filter::<Port>(&filter)?.len(),
Source::Netblocks => db.filter::<Netblock>(&filter)?.len(),
Source::CryptoAddrs(currency) => db.filter_with_param::<CryptoAddr>(&filter, currency.as_ref())?.len(),
Source::Notifications => bail!("Notifications can't be set as target"),
Source::KeyRing(namespace) => rl.keyring().list_for(&namespace).len(),
};
Ok(num)

View File

@@ -28,10 +28,8 @@ pub struct Args {
}
fn delete(workspace: Workspace, force: bool) -> Result<()> {
if !force {
if !utils::no_else_yes(&format!("Do you really want to delete {:?}", workspace.as_str()))? {
return Ok(());
}
if !force && !utils::no_else_yes(&format!("Do you really want to delete {:?}", workspace.as_str()))? {
return Ok(());
}
term::info(&format!("Deleting workspace: {:?}", workspace.as_str()));

View File

@@ -1,11 +1,9 @@
use dirs;
use crate::errors::*;
use crate::notify::NotificationConfig;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::net::SocketAddr;
use toml;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Config {
@@ -15,6 +13,8 @@ pub struct Config {
pub namespaces: HashMap<String, PathBuf>,
#[serde(default)]
pub network: NetworkConfig,
#[serde(default)]
pub notifications: HashMap<String, NotificationConfig>,
}
impl Config {
@@ -24,7 +24,7 @@ impl Config {
}
pub fn path() -> Result<PathBuf> {
let path = dirs::config_dir()
let path = dirs_next::config_dir()
.ok_or_else(|| format_err!("Failed to find config directory"))?;
let path = path.join("sn0int.toml");
Ok(path)

View File

@@ -1,6 +1,5 @@
use crate::errors::*;
use diesel;
use diesel::expression::SqlLiteral;
use diesel::expression::sql_literal::sql;
use diesel::sql_types::Bool;
@@ -25,10 +24,7 @@ pub enum DbChange {
impl DbChange {
pub fn is_some(&self) -> bool {
match self {
DbChange::None => false,
_ => true,
}
!matches!(self, DbChange::None)
}
}

View File

@@ -1,10 +1,13 @@
use crate::errors::*;
use crate::db::{Database, Table};
use crate::schema::*;
use crate::errors::*;
use crate::models::*;
use crate::notify::{self, Notification};
use crate::schema::*;
use crate::shell::Shell;
use crate::term::{self, Term};
use chrono::{NaiveDateTime, Duration, Utc};
use diesel;
use diesel::prelude::*;
use sn0int_std::ratelimits::Ratelimiter;
#[derive(Identifiable, Queryable, AsChangeset, PartialEq, Debug)]
@@ -13,26 +16,11 @@ pub struct Ttl {
pub id: i32,
pub family: String,
pub key: i32,
pub expire: NaiveDateTime,
}
#[derive(Insertable)]
#[table_name="ttls"]
pub struct NewTtl<'a> {
pub family: &'a str,
pub key: i32,
pub value: String,
pub expire: NaiveDateTime,
}
impl Ttl {
pub fn new(obj: &Insert, key: i32, expire: NaiveDateTime) -> NewTtl {
NewTtl {
family: obj.table(),
key,
expire,
}
}
pub fn find(obj: &Insert, my_key: i32, db: &Database) -> Result<Option<Ttl>> {
use crate::schema::ttls::dsl::*;
@@ -59,7 +47,7 @@ impl Ttl {
expire_at.naive_utc()
}
pub fn create(obj: &Insert, key: i32, ttl: i32, db: &Database) -> Result<()> {
pub fn create(obj: &Insert, key: i32, value: String, ttl: i32, db: &Database) -> Result<()> {
debug!("Creating ttl on record");
let expire = Self::ttl_to_datetime(ttl);
@@ -67,6 +55,7 @@ impl Ttl {
.values(NewTtl {
family: obj.table(),
key,
value,
expire,
})
.execute(db.db())?;
@@ -127,12 +116,31 @@ impl Ttl {
}
}
pub fn reap_expired(db: &Database) -> Result<()> {
#[derive(Insertable)]
#[table_name="ttls"]
pub struct NewTtl<'a> {
pub family: &'a str,
pub key: i32,
pub value: String,
pub expire: NaiveDateTime,
}
pub fn reap_expired(rl: &mut Shell) -> Result<()> {
debug!("Reaping expired entities");
for expired in Ttl::expired(db)? {
let mut ratelimit = Ratelimiter::new();
for expired in Ttl::expired(rl.db())? {
debug!("Expired: {:?}", expired);
expired.delete(db)?;
expired.delete(rl.db())?;
let subject = format!("Deleted {} {:?}", &expired.family, &expired.value);
let topic = &format!("db:{}:{}:delete", &expired.family, &expired.value);
if let Err(err) = notify::trigger_notify_event(rl, &mut Term, &mut ratelimit, &topic, &Notification {
subject,
body: None,
}) {
term::error(&format!("Failed to send notifications: {}", err));
}
}
debug!("Finished reaping expired entities");

View File

@@ -5,17 +5,17 @@ use crate::engine::{Environment, IpcChild};
use crate::geoip::{MaxmindReader, GeoIP, AsnDB};
use crate::hlua::{self, AnyLuaValue};
use crate::keyring::KeyRingEntry;
use crate::lazy::Lazy;
use crate::models::*;
use crate::psl::{Psl, PslReader};
use crate::lazy::Lazy;
use crate::ratelimits::RatelimitResponse;
use crate::runtime;
use crate::sockets::{Socket, SocketOptions, TlsData};
use crate::utils;
use crate::web::{HttpSession, HttpRequest, RequestOptions};
use crate::websockets::{WebSocket, WebSocketOptions};
use crate::worker::{Event, LogEvent, DatabaseEvent, DatabaseResponse, StdioEvent, RatelimitEvent};
use crate::ratelimits::RatelimitResponse;
use chrootable_https::{self, Resolver};
use serde_json;
use sn0int_std::blobs::{Blob, BlobState};
use sn0int_std::mqtt::{MqttClient, MqttOptions};
use sn0int_std::web::WebState;
@@ -24,8 +24,6 @@ use std::result;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::thread;
use rand::prelude::*;
use rand::distributions::Alphanumeric;
pub trait State {
@@ -80,14 +78,14 @@ pub trait State {
}
fn db_insert(&self, object: Insert) -> Result<DatabaseResponse> {
self.send(&Event::Database(DatabaseEvent::Insert(object)));
self.send(&Event::Database(Box::new(DatabaseEvent::Insert(object))));
self.db_recv()
.context("Failed to add to database")
.map_err(Error::from)
}
fn db_insert_ttl(&self, object: Insert, ttl: i32) -> Result<DatabaseResponse> {
self.send(&Event::Database(DatabaseEvent::InsertTtl((object, ttl))));
self.send(&Event::Database(Box::new(DatabaseEvent::InsertTtl((object, ttl)))));
self.db_recv()
.context("Failed to add to database")
.map_err(Error::from)
@@ -96,7 +94,7 @@ pub trait State {
fn db_activity(&self, activity: InsertActivity) -> Result<bool> {
let activity = activity.try_into_new()?;
self.send(&Event::Database(DatabaseEvent::Activity(activity)));
self.send(&Event::Database(Box::new(DatabaseEvent::Activity(activity))));
let r = self.db_recv()
.context("Failed to log activity")?;
@@ -108,14 +106,14 @@ pub trait State {
}
fn db_select(&self, family: Family, value: String) -> Result<DatabaseResponse> {
self.send(&Event::Database(DatabaseEvent::Select((family, value))));
self.send(&Event::Database(Box::new(DatabaseEvent::Select((family, value)))));
self.db_recv()
.context("Failed to query database")
.map_err(Error::from)
}
fn db_update(&self, object: String, update: Update) -> Result<DatabaseResponse> {
self.send(&Event::Database(DatabaseEvent::Update((object, update))));
fn db_update(&self, family: Family, value: String, update: Update) -> Result<DatabaseResponse> {
self.send(&Event::Database(Box::new(DatabaseEvent::Update((family, value, update)))));
self.db_recv()
.context("Failed to update database")
.map_err(Error::from)
@@ -152,7 +150,7 @@ pub trait State {
#[inline]
fn random_id(&self) -> String {
thread_rng().sample_iter(&Alphanumeric).take(16).collect()
utils::random_string(16)
}
fn keyring(&self, namespace: &str) -> Vec<&KeyRingEntry>;
@@ -373,7 +371,7 @@ impl State for LuaState {
let mtx = self.http_sessions.lock().unwrap();
let session = mtx.get(session_id).expect("Invalid session reference"); // TODO
HttpRequest::new(&session, method, url, options)
HttpRequest::new(&session, method, url, options, || format!("sn0int/{}", env!("CARGO_PKG_VERSION")))
}
fn get_blob(&self, id: &str) -> Result<Arc<Blob>> {
@@ -620,6 +618,11 @@ impl Script {
})
}
#[inline]
pub fn code(&self) -> &str {
&self.code
}
pub fn run(&self, env: Environment,
tx: Arc<Mutex<Box<dyn IpcChild>>>,
arg: AnyLuaValue,
@@ -681,6 +684,6 @@ a.prod.fastly.net
geoip,
asn,
};
self.run(env, DummyIpcChild::new(), AnyLuaValue::LuaNil)
self.run(env, DummyIpcChild::create(), AnyLuaValue::LuaNil)
}
}

View File

@@ -14,14 +14,13 @@ use std::sync::{Arc, Mutex};
use crate::engine::ctx::Script;
use crate::ipc::child::IpcChild;
use sn0int_common::ModuleID;
use sn0int_common::metadata::{Metadata, Source};
use sn0int_common::metadata::{Metadata, Source, Stealth};
use chrootable_https::dns::Resolver;
use crate::psl::PslReader;
use crate::paths;
use std::cmp::Ordering;
use std::path::Path;
use crate::term;
use crate::worker;
pub mod ctx;
pub use sn0int_std::engine::structs;
@@ -68,11 +67,9 @@ impl<'a> Library<'a> {
}
pub fn reload_modules(&mut self) -> Result<usize> {
let modules = worker::spawn_fn("Loading modules", || {
self.reload_modules_quiet()
.context("Failed to load modules")?;
Ok(self.list().len())
}, true)?;
self.reload_modules_quiet()
.context("Failed to load modules")?;
let modules = self.list().len();
term::info(&format!("Loaded {} modules", modules));
Ok(modules)
}
@@ -124,7 +121,7 @@ impl<'a> Library<'a> {
let folder = folder.strip_prefix("~/")
.unwrap_or(&folder);
dirs::home_dir()
dirs_next::home_dir()
.ok_or_else(|| format_err!("Failed to find home folder"))?
.join(folder)
};
@@ -229,6 +226,8 @@ pub struct Module {
version: String,
source: Option<Source>,
keyring_access: Vec<String>,
stealth: Stealth,
private_module: bool,
script: Script,
}
@@ -240,7 +239,7 @@ impl Module {
.context("Failed to read module")?;
let metadata = code.parse::<Metadata>()
.context("Failed to parse module metadata")?;
.map_err(|e| format_err!("Failed to parse module metadata: {}", e))?;
let script = Script::load_unchecked(code)?;
@@ -251,11 +250,18 @@ impl Module {
version: metadata.version,
source: metadata.source,
keyring_access: metadata.keyring_access,
stealth: metadata.stealth,
private_module,
script,
})
}
#[inline]
pub fn author(&self) -> &str {
&self.author
}
#[inline]
pub fn name(&self) -> &str {
&self.name
@@ -293,11 +299,21 @@ impl Module {
&self.keyring_access
}
#[inline]
pub fn stealth(&self) -> &Stealth {
&self.stealth
}
#[inline]
pub fn is_private(&self) -> bool {
self.private_module
}
#[inline]
pub fn code(&self) -> &str {
self.script.code()
}
pub fn run(&self, env: Environment, ipc_child: Arc<Mutex<Box<dyn IpcChild>>>, arg: LuaJsonValue) -> Result<()> {
debug!("Executing lua script {}", self.canonical());
self.script.run(env, ipc_child, arg.into())

View File

@@ -3,15 +3,12 @@ use crate::ipc::common::*;
use crate::engine::Environment;
use crate::geoip::MaxmindReader;
use crate::psl::PslReader;
use serde_json;
use crate::worker::Event;
use std::fmt::Debug;
use std::io::prelude::*;
use std::io::{self, Stdin, Stdout};
use std::sync::{Arc, Mutex};
pub trait IpcChild: Debug {
fn send(&mut self, event: &Event) -> Result<()>;
@@ -65,7 +62,7 @@ impl IpcChild for StdioIpcChild {
pub struct DummyIpcChild;
impl DummyIpcChild {
pub fn new() -> Arc<Mutex<Box<dyn IpcChild>>> {
pub fn create() -> Arc<Mutex<Box<dyn IpcChild>>> {
Arc::new(Mutex::new(Box::new(DummyIpcChild)))
}
}

View File

@@ -2,12 +2,9 @@ use chrootable_https::dns::Resolver;
use crate::blobs::Blob;
use crate::engine::Module;
use crate::keyring::KeyRingEntry;
use serde_json;
use std::collections::HashMap;
use std::net::SocketAddr;
#[derive(Debug, Serialize, Deserialize)]
pub struct StartCommand {
pub verbose: u64,

View File

@@ -4,9 +4,7 @@ use chrootable_https::dns::Resolver;
use crate::blobs::Blob;
use crate::engine::Module;
use crate::keyring::KeyRingEntry;
use serde_json;
use crate::worker::{Event, Event2, LogEvent, ExitEvent, EventSender, EventWithCallback};
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
@@ -16,7 +14,6 @@ use std::net::SocketAddr;
use std::sync::mpsc;
use std::process::{Command, Child, Stdio, ChildStdin, ChildStdout};
pub struct IpcParent {
child: Child,
stdin: ChildStdin,
@@ -65,7 +62,7 @@ impl IpcParent {
pub fn send_struct<T: serde::Serialize>(&mut self, value: T, tx: &EventSender) {
let value = serde_json::to_value(value).expect("Failed to serialize reply");
if let Err(_) = self.send(&value) {
if self.send(&value).is_err() {
tx.send(Event2::Log(LogEvent::Error("Failed to send to child".into())));
}
}
@@ -125,7 +122,7 @@ pub fn run(module: Module,
let exit = loop {
match ipc_parent.recv()? {
Event::Log(event) => tx.send(Event2::Log(event)),
Event::Database(object) => ipc_parent.send_event_callback(object, &tx),
Event::Database(object) => ipc_parent.send_event_callback(*object, &tx),
Event::Stdio(object) => object.apply(&mut ipc_parent, tx, &mut reader),
Event::Ratelimit(req) => ipc_parent.send_event_callback(req, &tx),
Event::Blob(blob) => ipc_parent.send_event_callback(blob, &tx),

View File

@@ -1,6 +1,9 @@
#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
#![allow(clippy::large_enum_variant)]
#![warn(unused_extern_crates)]
#[macro_use] extern crate maplit;
use url;
use hlua_badtouch as hlua;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate structopt;
@@ -14,6 +17,7 @@ pub mod args;
pub mod auth;
pub mod autonoscope;
pub mod blobs;
pub mod cal;
pub mod cmd;
pub mod config;
use sn0int_std::crt;
@@ -33,6 +37,7 @@ use sn0int_std::lazy;
pub mod migrations;
pub mod models;
use sn0int_std::mqtt;
pub mod notify;
pub mod paths;
pub use sn0int_std::psl;
pub mod options;

View File

@@ -27,9 +27,9 @@ fn run_run(gargs: &Args, args: &args::Run, config: &Config) -> Result<()> {
let path = Path::new(&args.module);
let filename = path.file_stem()
.ok_or(format_err!("Failed to decode filename"))?
.ok_or_else(|| format_err!("Failed to decode filename"))?
.to_str()
.ok_or(format_err!("Failed to decode filename"))?;
.ok_or_else(|| format_err!("Failed to decode filename"))?;
Module::load(&path.to_path_buf(), "anonymous", &filename, true)
.context(format!("Failed to parse {:?}", path))?
@@ -71,6 +71,7 @@ fn run_cmd<T: cmd::Cmd>(gargs: &Args, args: T, config: &Config) -> Result<()> {
}
fn run_new(_gargs: &Args, args: &args::New) -> Result<()> {
// TODO: Add `-- Author: anonymous`
let boilerplate = b"-- Description: TODO your description here
-- Version: 0.1.0
-- License: GPL-3.0
@@ -125,6 +126,8 @@ fn run() -> Result<()> {
Some(SubCommand::Workspace(workspace)) => workspace.run(&config),
Some(SubCommand::Fsck(fsck)) => run_cmd(&args, fsck, &config),
Some(SubCommand::Export(export)) => run_cmd(&args, export, &config),
Some(SubCommand::Cal(cal)) => run_cmd(&args, cal, &config),
Some(SubCommand::Notify(notify)) => run_cmd(&args, notify, &config),
Some(SubCommand::Repl) => repl::run(&config),
Some(SubCommand::Paths) => paths::run(&config),
Some(SubCommand::Completions(completions)) => complete::run_generate(&completions),

View File

@@ -1,11 +1,9 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use chrono::NaiveDateTime;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="accounts"]
pub struct Account {
@@ -210,7 +208,7 @@ impl Detailed for Account {
displayname: self.displayname.clone(),
email: self.email.clone(),
url: self.url.clone(),
last_seen: self.last_seen.clone(),
last_seen: self.last_seen,
unscoped: self.unscoped,
birthday: self.birthday.clone(),
phonenumber: self.phonenumber.clone(),
@@ -358,13 +356,13 @@ impl Updateable<Account> for AccountUpdate {
Self::clear_if_equal(&mut self.profile_pic, &existing.profile_pic);
}
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);
Self::push_value(updates, "birthday", &self.birthday);
Self::push_value(updates, "phonenumber", &self.phonenumber);
Self::push_value(updates, "profile_pic", &self.profile_pic);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "displayname", &self.displayname, colors);
Self::push_value(updates, "email", &self.email, colors);
Self::push_value(updates, "url", &self.url, colors);
Self::push_value(updates, "last_seen", &self.last_seen, colors);
Self::push_value(updates, "birthday", &self.birthday, colors);
Self::push_value(updates, "phonenumber", &self.phonenumber, colors);
Self::push_value(updates, "profile_pic", &self.profile_pic, colors);
}
}

View File

@@ -1,6 +1,5 @@
use crate::errors::*;
use crate::schema::activity;
use diesel;
use diesel::prelude::*;
use diesel::query_builder::BoxedSelectStatement;
use crate::models::*;
@@ -8,7 +7,6 @@ use chrono::NaiveDateTime;
use std::convert::TryFrom;
use std::io::Write;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="activity"]
pub struct Activity {
@@ -37,16 +35,19 @@ impl Activity {
let mut query = activity.into_boxed();
if let Some(my_topic) = &filter.topic {
debug!("Filtering topic to be {:?}", my_topic);
query = query.filter(topic.eq(my_topic));
}
// "since" filter is not applied
if let Some(until) = &filter.until {
debug!("Filtering until <= {}", until);
query = query.filter(time.le(until));
}
if filter.location {
debug!("Filtering latitude and longitude != null");
query = query
.filter(latitude.is_not_null())
.filter(longitude.is_not_null());
@@ -61,6 +62,7 @@ impl Activity {
let mut query = Self::build_query_except_since(filter);
if let Some(since) = &filter.since {
debug!("Filtering since >= {}", since);
query = query.filter(time.ge(since));
}

View File

@@ -1,10 +1,8 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="breaches"]
pub struct Breach {

View File

@@ -1,9 +1,7 @@
use crate::errors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize)]
#[belongs_to(Breach)]
#[belongs_to(Email)]
@@ -136,8 +134,8 @@ impl Printable<PrintableBreachEmail> for BreachEmail {
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(),
breach: breach.value,
email: email.value,
password: self.password.clone(),
})
}
@@ -167,8 +165,8 @@ impl Printable<PrintableBreachEmail> for NewBreachEmail {
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(),
breach: breach.value,
email: email.value,
password: self.password.clone(),
})
}
@@ -211,7 +209,7 @@ impl Updateable<BreachEmail> for BreachEmailUpdate {
Self::clear_if_equal(&mut self.password, &existing.password);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "password", &self.password);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "password", &self.password, colors);
}
}

View File

@@ -1,11 +1,9 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use chrono::NaiveDateTime;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="cryptoaddrs"]
pub struct CryptoAddr {
@@ -220,11 +218,11 @@ impl Detailed for CryptoAddr {
id: self.id,
value: self.value.to_string(),
currency: self.currency.clone(),
denominator: self.denominator.clone(),
balance: self.balance.clone(),
received: self.received.clone(),
first_seen: self.first_seen.clone(),
last_withdrawal: self.last_withdrawal.clone(),
denominator: self.denominator,
balance: self.balance,
received: self.received,
first_seen: self.first_seen,
last_withdrawal: self.last_withdrawal,
unscoped: self.unscoped,
description: self.description.clone(),
})
@@ -342,14 +340,14 @@ impl Updateable<CryptoAddr> for CryptoAddrUpdate {
Self::clear_if_equal(&mut self.description, &existing.description);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "currency", &self.currency);
Self::push_value(updates, "denominator", &self.denominator);
Self::push_value(updates, "balance", &self.balance);
Self::push_value(updates, "received", &self.received);
Self::push_value(updates, "first_seen", &self.first_seen);
Self::push_value(updates, "last_withdrawal", &self.last_withdrawal);
Self::push_value(updates, "description", &self.description);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "currency", &self.currency, colors);
Self::push_value(updates, "denominator", &self.denominator, colors);
Self::push_value(updates, "balance", &self.balance, colors);
Self::push_value(updates, "received", &self.received, colors);
Self::push_value(updates, "first_seen", &self.first_seen, colors);
Self::push_value(updates, "last_withdrawal", &self.last_withdrawal, colors);
Self::push_value(updates, "description", &self.description, colors);
}
}

View File

@@ -1,11 +1,9 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use chrono::NaiveDateTime;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="devices"]
pub struct Device {
@@ -216,7 +214,7 @@ impl Detailed for Device {
ipaddr,
network,
unscoped: self.unscoped,
last_seen: self.last_seen.clone(),
last_seen: self.last_seen,
})
}
}
@@ -332,10 +330,10 @@ impl Updateable<Device> for DeviceUpdate {
Self::clear_if_equal(&mut self.last_seen, &existing.last_seen);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "name", &self.name);
Self::push_value(updates, "hostname", &self.hostname);
Self::push_value(updates, "vendor", &self.vendor);
Self::push_value(updates, "last_seen", &self.last_seen);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "name", &self.name, colors);
Self::push_value(updates, "hostname", &self.hostname, colors);
Self::push_value(updates, "vendor", &self.vendor, colors);
Self::push_value(updates, "last_seen", &self.last_seen, colors);
}
}

View File

@@ -1,10 +1,8 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="domains"]
pub struct Domain {
@@ -122,24 +120,6 @@ impl Domain {
}
}
pub struct PrintableDomain {
value: String,
}
impl fmt::Display for PrintableDomain {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{:?}", self.value)
}
}
impl Printable<PrintableDomain> for Domain {
fn printable(&self, _db: &Database) -> Result<PrintableDomain> {
Ok(PrintableDomain {
value: self.value.to_string(),
})
}
}
pub struct DetailedDomain {
id: i32,
value: String,
@@ -222,14 +202,6 @@ impl Upsertable<Domain> for NewDomain {
}
}
impl Printable<PrintableDomain> for NewDomain {
fn printable(&self, _db: &Database) -> Result<PrintableDomain> {
Ok(PrintableDomain {
value: self.value.to_string(),
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InsertDomain {
pub value: String,
@@ -240,6 +212,9 @@ impl InsertToNew for InsertDomain {
fn try_into_new(self) -> Result<NewDomain> {
let value = self.value.to_lowercase();
if value.contains('*') {
bail!("Asterisks inside domains are not valid");
}
Ok(NewDomain {
value,
unscoped: false,

View File

@@ -1,10 +1,8 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="emails"]
pub struct Email {
@@ -335,9 +333,9 @@ impl Updateable<Email> for EmailUpdate {
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);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "displayname", &self.displayname, colors);
Self::push_value(updates, "valid", &self.valid, colors);
}
}

View File

@@ -1,13 +1,11 @@
use crate::errors::*;
use crate::fmt::colors::*;
use chrono::NaiveDateTime;
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="images"]
pub struct Image {
@@ -174,8 +172,8 @@ impl Printable<PrintableImage> for Image {
Ok(PrintableImage {
value: self.value.to_string(),
filename: self.filename.clone(),
width: self.width.clone(),
height: self.height.clone(),
width: self.width,
height: self.height,
})
}
}
@@ -256,14 +254,14 @@ impl Detailed for Image {
filename: self.filename.clone(),
mime: self.mime.clone(),
width: self.width.clone(),
height: self.height.clone(),
created: self.created.clone(),
width: self.width,
height: self.height,
created: self.created,
latitude: self.latitude.clone(),
longitude: self.longitude.clone(),
latitude: self.latitude,
longitude: self.longitude,
nudity: self.nudity.clone(),
nudity: self.nudity,
ahash: self.ahash.clone(),
dhash: self.dhash.clone(),
phash: self.phash.clone(),
@@ -341,8 +339,8 @@ impl Printable<PrintableImage> for NewImage {
Ok(PrintableImage {
value: self.value.to_string(),
filename: self.filename.clone(),
width: self.width.clone(),
height: self.height.clone(),
width: self.width,
height: self.height,
})
}
}
@@ -464,19 +462,19 @@ impl Updateable<Image> for ImageUpdate {
Self::clear_if_equal(&mut self.phash, &existing.phash);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "filename", &self.filename);
Self::push_value(updates, "mime", &self.mime);
Self::push_value(updates, "width", &self.width);
Self::push_value(updates, "height", &self.height);
Self::push_value(updates, "created", &self.created);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "filename", &self.filename, colors);
Self::push_value(updates, "mime", &self.mime, colors);
Self::push_value(updates, "width", &self.width, colors);
Self::push_value(updates, "height", &self.height, colors);
Self::push_value(updates, "created", &self.created, colors);
Self::push_value(updates, "latitude", &self.latitude);
Self::push_value(updates, "longitude", &self.longitude);
Self::push_value(updates, "latitude", &self.latitude, colors);
Self::push_value(updates, "longitude", &self.longitude, colors);
Self::push_value(updates, "nudity", &self.nudity);
Self::push_value(updates, "ahash", &self.ahash);
Self::push_value(updates, "dhash", &self.dhash);
Self::push_value(updates, "phash", &self.phash);
Self::push_value(updates, "nudity", &self.nudity, colors);
Self::push_value(updates, "ahash", &self.ahash, colors);
Self::push_value(updates, "dhash", &self.dhash, colors);
Self::push_value(updates, "phash", &self.phash, colors);
}
}

View File

@@ -2,12 +2,10 @@ use crate::errors::*;
use crate::fmt::Write;
use crate::fmt::colors::*;
use crate::models::*;
use diesel;
use diesel::prelude::*;
use std::net;
use std::result;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="ipaddrs"]
pub struct IpAddr {
@@ -444,17 +442,17 @@ impl Updateable<IpAddr> for IpAddrUpdate {
Self::clear_if_equal(&mut self.reverse_dns, &existing.reverse_dns);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "continent", &self.continent);
Self::push_value(updates, "continent_code", &self.continent_code);
Self::push_value(updates, "country", &self.country);
Self::push_value(updates, "country_code", &self.country_code);
Self::push_value(updates, "city", &self.city);
Self::push_value(updates, "latitude", &self.latitude);
Self::push_value(updates, "longitude", &self.longitude);
Self::push_value(updates, "asn", &self.asn);
Self::push_value(updates, "as_org", &self.as_org);
Self::push_value(updates, "description", &self.description);
Self::push_value(updates, "reverse_dns", &self.reverse_dns);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "continent", &self.continent, colors);
Self::push_value(updates, "continent_code", &self.continent_code, colors);
Self::push_value(updates, "country", &self.country, colors);
Self::push_value(updates, "country_code", &self.country_code, colors);
Self::push_value(updates, "city", &self.city, colors);
Self::push_value(updates, "latitude", &self.latitude, colors);
Self::push_value(updates, "longitude", &self.longitude, colors);
Self::push_value(updates, "asn", &self.asn, colors);
Self::push_value(updates, "as_org", &self.as_org, colors);
Self::push_value(updates, "description", &self.description, colors);
Self::push_value(updates, "reverse_dns", &self.reverse_dns, colors);
}
}

View File

@@ -1,9 +1,10 @@
use crate::db::{Database, Table, Filter, Family};
use crate::engine::ctx::State;
use crate::errors::*;
use crate::db::{Database, Table, Filter};
use crate::fmt;
use crate::schema::*;
use std::borrow::Cow;
use std::sync::Arc;
use crate::engine::ctx::State;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -28,39 +29,39 @@ pub enum Insert {
}
impl Insert {
pub fn label(&self, db: &Database) -> Result<String> {
let label = match self {
Insert::Domain(x) => format!("{:?}", x.value),
Insert::Subdomain(x) => format!("{:?}", x.value),
Insert::IpAddr(x) => format!("{:?}", x.value),
pub fn value(&self, db: &Database) -> Result<Cow<String>> {
let value = match self {
Insert::Domain(x) => Cow::Borrowed(&x.value),
Insert::Subdomain(x) => Cow::Borrowed(&x.value),
Insert::IpAddr(x) => Cow::Borrowed(&x.value),
Insert::SubdomainIpAddr(x) => {
let subdomain = Subdomain::by_id(db, x.subdomain_id)?;
let ipaddr = IpAddr::by_id(db, x.ip_addr_id)?;
format!("{:?}+{:?}", subdomain.value, ipaddr.value)
Cow::Owned(format!("{}+{}", subdomain.value, ipaddr.value))
},
Insert::Url(x) => format!("{:?}", x.value),
Insert::Email(x) => format!("{:?}", x.value),
Insert::PhoneNumber(x) => format!("{:?}", x.value),
Insert::Device(x) => format!("{:?}", x.value),
Insert::Network(x) => format!("{:?}", x.value),
Insert::Url(x) => Cow::Borrowed(&x.value),
Insert::Email(x) => Cow::Borrowed(&x.value),
Insert::PhoneNumber(x) => Cow::Borrowed(&x.value),
Insert::Device(x) => Cow::Borrowed(&x.value),
Insert::Network(x) => Cow::Borrowed(&x.value),
Insert::NetworkDevice(x) => {
let network = Network::by_id(db, x.network_id)?;
let device = Device::by_id(db, x.device_id)?;
format!("{:?}+{:?}", network.value, device.value)
Cow::Owned(format!("{}+{}", network.value, device.value))
},
Insert::Account(x) => format!("{:?}", x.value),
Insert::Breach(x) => format!("{:?}", x.value),
Insert::Account(x) => Cow::Borrowed(&x.value),
Insert::Breach(x) => Cow::Borrowed(&x.value),
Insert::BreachEmail(x) => {
let breach = Breach::by_id(db, x.breach_id)?;
let email = Email::by_id(db, x.email_id)?;
format!("{:?}+{:?}", breach.value, email.value)
Cow::Owned(format!("{}+{}", breach.value, email.value))
}
Insert::Image(x) => format!("{:?}", x.value),
Insert::Port(x) => format!("{:?}", x.value),
Insert::Netblock(x) => format!("{:?}", x.value),
Insert::CryptoAddr(x) => format!("{:?}", x.value),
Insert::Image(x) => Cow::Borrowed(&x.value),
Insert::Port(x) => Cow::Borrowed(&x.value),
Insert::Netblock(x) => Cow::Borrowed(&x.value),
Insert::CryptoAddr(x) => Cow::Borrowed(&x.value),
};
Ok(label)
Ok(value)
}
#[inline]
@@ -68,26 +69,27 @@ impl Insert {
Table::from(self).into()
}
pub fn printable(&self, db: &Database) -> Result<String> {
Ok(match self {
Insert::Domain(x) => format!("Domain: {}", x.printable(db)?),
Insert::Subdomain(x) => format!("Subdomain: {}", x.printable(db)?),
Insert::IpAddr(x) => format!("IpAddr: {}", x.printable(db)?),
Insert::SubdomainIpAddr(x) => x.printable(db)?.to_string(),
Insert::Url(x) => format!("Url: {}", x.printable(db)?),
Insert::Email(x) => format!("Email: {}", x.printable(db)?),
Insert::PhoneNumber(x) => format!("PhoneNumber: {}", x.printable(db)?),
Insert::Device(x) => format!("Device: {}", x.printable(db)?),
Insert::Network(x) => format!("Network: {}", x.printable(db)?),
Insert::NetworkDevice(x) => x.printable(db)?.to_string(),
Insert::Account(x) => format!("Account: {}", x.printable(db)?),
Insert::Breach(x) => format!("Breach: {}", x.printable(db)?),
Insert::BreachEmail(x) => x.printable(db)?.to_string(),
Insert::Image(x) => format!("Image: {}", x.printable(db)?),
Insert::Port(x) => format!("Port: {}", x.printable(db)?),
Insert::Netblock(x) => format!("Netblock: {}", x.printable(db)?),
Insert::CryptoAddr(x) => format!("CryptoAddr: {}", x.printable(db)?),
})
#[inline]
pub fn family(&self) -> &str {
match self {
Insert::Domain(_) => Family::Domain.as_str(),
Insert::Subdomain(_) => Family::Subdomain.as_str(),
Insert::IpAddr(_) => Family::Ipaddr.as_str(),
Insert::SubdomainIpAddr(_) => Family::SubdomainIpaddr.as_str(),
Insert::Url(_) => Family::Url.as_str(),
Insert::Email(_) => Family::Email.as_str(),
Insert::PhoneNumber(_) => Family::Phonenumber.as_str(),
Insert::Device(_) => Family::Device.as_str(),
Insert::Network(_) => Family::Network.as_str(),
Insert::NetworkDevice(_) => Family::NetworkDevice.as_str(),
Insert::Account(_) => Family::Account.as_str(),
Insert::Breach(_) => Family::Breach.as_str(),
Insert::BreachEmail(_) => Family::BreachEmail.as_str(),
Insert::Image(_) => Family::Image.as_str(),
Insert::Port(_) => Family::Port.as_str(),
Insert::Netblock(_) => Family::Netblock.as_str(),
Insert::CryptoAddr(_) => Family::Cryptoaddr.as_str(),
}
}
}
@@ -152,25 +154,42 @@ impl Update {
Update::CryptoAddr(update) => update.is_dirty(),
}
}
}
impl fmt::Display for Update {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
pub fn to_plain_str(&self) -> String {
match self {
Update::Subdomain(update) => write!(w, "{}", update.to_string()),
Update::IpAddr(update) => write!(w, "{}", update.to_string()),
Update::Url(update) => write!(w, "{}", update.to_string()),
Update::Email(update) => write!(w, "{}", update.to_string()),
Update::PhoneNumber(update) => write!(w, "{}", update.to_string()),
Update::Device(update) => write!(w, "{}", update.to_string()),
Update::Network(update) => write!(w, "{}", update.to_string()),
Update::NetworkDevice(update) => write!(w, "{}", update.to_string()),
Update::Account(update) => write!(w, "{}", update.to_string()),
Update::BreachEmail(update) => write!(w, "{}", update.to_string()),
Update::Image(update) => write!(w, "{}", update.to_string()),
Update::Port(update) => write!(w, "{}", update.to_string()),
Update::Netblock(update) => write!(w, "{}", update.to_string()),
Update::CryptoAddr(update) => write!(w, "{}", update.to_string()),
Update::Subdomain(update) => update.to_plain_str(),
Update::IpAddr(update) => update.to_plain_str(),
Update::Url(update) => update.to_plain_str(),
Update::Email(update) => update.to_plain_str(),
Update::PhoneNumber(update) => update.to_plain_str(),
Update::Device(update) => update.to_plain_str(),
Update::Network(update) => update.to_plain_str(),
Update::NetworkDevice(update) => update.to_plain_str(),
Update::Account(update) => update.to_plain_str(),
Update::BreachEmail(update) => update.to_plain_str(),
Update::Image(update) => update.to_plain_str(),
Update::Port(update) => update.to_plain_str(),
Update::Netblock(update) => update.to_plain_str(),
Update::CryptoAddr(update) => update.to_plain_str(),
}
}
pub fn to_term_str(&self) -> String {
match self {
Update::Subdomain(update) => update.to_term_str(),
Update::IpAddr(update) => update.to_term_str(),
Update::Url(update) => update.to_term_str(),
Update::Email(update) => update.to_term_str(),
Update::PhoneNumber(update) => update.to_term_str(),
Update::Device(update) => update.to_term_str(),
Update::Network(update) => update.to_term_str(),
Update::NetworkDevice(update) => update.to_term_str(),
Update::Account(update) => update.to_term_str(),
Update::BreachEmail(update) => update.to_term_str(),
Update::Image(update) => update.to_term_str(),
Update::Port(update) => update.to_term_str(),
Update::Netblock(update) => update.to_term_str(),
Update::CryptoAddr(update) => update.to_term_str(),
}
}
}
@@ -274,9 +293,15 @@ impl Upsert for NullUpdate {
}
pub trait Updateable<M> {
fn to_string(&self) -> String {
fn to_plain_str(&self) -> String {
let mut updates = Vec::new();
self.fmt(&mut updates);
self.fmt(&mut updates, false);
updates.join(", ")
}
fn to_term_str(&self) -> String {
let mut updates = Vec::new();
self.fmt(&mut updates, true);
updates.join(", ")
}
@@ -304,22 +329,31 @@ pub trait Updateable<M> {
fn changeset(&mut self, existing: &M);
#[inline]
fn push_value<D: fmt::Debug>(updates: &mut Vec<String>, name: &str, value: &Option<D>) {
fn push_value<D: fmt::Debug>(updates: &mut Vec<String>, name: &str, value: &Option<D>, colors: bool) {
if let Some(v) = value {
updates.push(format!("{} => \x1b[33m{:?}\x1b[0m", name, v));
if colors {
updates.push(format!("{} => \x1b[33m{:?}\x1b[0m", name, v));
} else {
updates.push(format!("{} => {:?}", name, v));
}
}
}
#[inline]
fn push_raw<T: AsRef<str>>(updates: &mut Vec<String>, name: &str, value: Option<T>) {
fn push_raw<T: AsRef<str>>(updates: &mut Vec<String>, name: &str, value: Option<T>, colors: bool) {
if let Some(v) = value {
updates.push(format!("{} => \x1b[33m{}\x1b[0m", name, v.as_ref()));
if colors {
updates.push(format!("{} => \x1b[33m{}\x1b[0m", name, v.as_ref()));
} else {
updates.push(format!("{} => {}", name, v.as_ref()));
}
}
}
fn fmt(&self, updates: &mut Vec<String>);
fn fmt(&self, updates: &mut Vec<String>, colors: bool);
}
// TODO: Printable could probably be dropped
pub trait Printable<T: Sized> {
fn printable(&self, db: &Database) -> Result<T>;
}

View File

@@ -1,10 +1,7 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use ipnetwork;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="netblocks"]
@@ -307,9 +304,9 @@ impl Updateable<Netblock> for NetblockUpdate {
Self::clear_if_equal(&mut self.description, &existing.description);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "asn", &self.asn);
Self::push_value(updates, "as_org", &self.as_org);
Self::push_value(updates, "description", &self.description);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "asn", &self.asn, colors);
Self::push_value(updates, "as_org", &self.as_org, colors);
Self::push_value(updates, "description", &self.description, colors);
}
}

View File

@@ -1,10 +1,8 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="networks"]
pub struct Network {
@@ -199,8 +197,8 @@ impl Detailed for Network {
id: self.id,
value: self.value.to_string(),
unscoped: self.unscoped,
latitude: self.latitude.clone(),
longitude: self.longitude.clone(),
latitude: self.latitude,
longitude: self.longitude,
description: self.description.clone(),
devices,
})
@@ -310,9 +308,9 @@ impl Updateable<Network> for NetworkUpdate {
Self::clear_if_equal(&mut self.description, &existing.description);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "latitude", &self.latitude);
Self::push_value(updates, "longitude", &self.longitude);
Self::push_value(updates, "description", &self.description);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "latitude", &self.latitude, colors);
Self::push_value(updates, "longitude", &self.longitude, colors);
Self::push_value(updates, "description", &self.description, colors);
}
}

View File

@@ -1,10 +1,8 @@
use crate::errors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use chrono::NaiveDateTime;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize)]
#[belongs_to(Network)]
#[belongs_to(Device)]
@@ -116,8 +114,8 @@ impl Printable<PrintableNetworkDevice> for NetworkDevice {
let network = Network::by_id(db, self.network_id)?;
let device = Device::by_id(db, self.device_id)?;
Ok(PrintableNetworkDevice {
network: network.value.to_string(),
device: device.value.to_string(),
network: network.value,
device: device.value,
})
}
}
@@ -148,8 +146,8 @@ impl Printable<PrintableNetworkDevice> for NewNetworkDevice {
let network = Network::by_id(db, self.network_id)?;
let device = Device::by_id(db, self.device_id)?;
Ok(PrintableNetworkDevice {
network: network.value.to_string(),
device: device.value.to_string(),
network: network.value,
device: device.value,
})
}
}
@@ -194,8 +192,8 @@ impl Updateable<NetworkDevice> for NetworkDeviceUpdate {
Self::clear_if_equal(&mut self.last_seen, &existing.last_seen);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "ipaddr", &self.ipaddr);
Self::push_value(updates, "last_seen", &self.last_seen);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "ipaddr", &self.ipaddr, colors);
Self::push_value(updates, "last_seen", &self.last_seen, colors);
}
}

View File

@@ -2,10 +2,8 @@ use crate::errors::*;
use crate::fmt::colors::*;
use crate::models::*;
use chrono::NaiveDateTime;
use diesel;
use diesel::prelude::*;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
#[table_name="phonenumbers"]
pub struct PhoneNumber {
@@ -377,16 +375,16 @@ impl Updateable<PhoneNumber> for PhoneNumberUpdate {
Self::clear_if_equal(&mut self.caller_type, &existing.caller_type);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "name", &self.name);
Self::push_value(updates, "valid", &self.valid);
Self::push_value(updates, "last_online", &self.last_online);
Self::push_value(updates, "country", &self.country);
Self::push_value(updates, "carrier", &self.carrier);
Self::push_value(updates, "line", &self.line);
Self::push_value(updates, "is_ported", &self.is_ported);
Self::push_value(updates, "last_ported", &self.last_ported);
Self::push_value(updates, "caller_name", &self.caller_name);
Self::push_value(updates, "caller_type", &self.caller_type);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "name", &self.name, colors);
Self::push_value(updates, "valid", &self.valid, colors);
Self::push_value(updates, "last_online", &self.last_online, colors);
Self::push_value(updates, "country", &self.country, colors);
Self::push_value(updates, "carrier", &self.carrier, colors);
Self::push_value(updates, "line", &self.line, colors);
Self::push_value(updates, "is_ported", &self.is_ported, colors);
Self::push_value(updates, "last_ported", &self.last_ported, colors);
Self::push_value(updates, "caller_name", &self.caller_name, colors);
Self::push_value(updates, "caller_type", &self.caller_type, colors);
}
}

View File

@@ -2,11 +2,9 @@ use crate::errors::*;
use crate::fmt::Write;
use crate::fmt::colors::*;
use crate::models::*;
use diesel;
use diesel::prelude::*;
use std::net::{self, SocketAddr};
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)]
#[belongs_to(IpAddr)]
#[table_name="ports"]
@@ -337,10 +335,10 @@ impl Updateable<Port> for PortUpdate {
Self::clear_if_equal(&mut self.version, &existing.version);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "status", &self.status);
Self::push_value(updates, "banner", &self.banner);
Self::push_value(updates, "service", &self.service);
Self::push_value(updates, "version", &self.version);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "status", &self.status, colors);
Self::push_value(updates, "banner", &self.banner, colors);
Self::push_value(updates, "service", &self.service, colors);
Self::push_value(updates, "version", &self.version, colors);
}
}

View File

@@ -1,11 +1,9 @@
use crate::errors::*;
use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::result;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)]
#[belongs_to(Domain)]
#[table_name="subdomains"]
@@ -258,6 +256,9 @@ impl InsertToNew for InsertSubdomain {
fn try_into_new(self) -> Result<NewSubdomain> {
let value = self.value.to_lowercase();
if value.contains('*') {
bail!("Asterisks inside domains are not valid");
}
Ok(NewSubdomain {
domain_id: self.domain_id,
value,
@@ -294,7 +295,7 @@ impl Updateable<Subdomain> for SubdomainUpdate {
Self::clear_if_equal(&mut self.resolvable, &existing.resolvable);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "resolvable", &self.resolvable);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "resolvable", &self.resolvable, colors);
}
}

View File

@@ -1,10 +1,8 @@
use crate::errors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::net;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize)]
#[belongs_to(Subdomain)]
#[belongs_to(IpAddr)]
@@ -108,7 +106,7 @@ impl Printable<PrintableSubdomainIpAddr> for SubdomainIpAddr {
let subdomain = Subdomain::by_id(db, self.subdomain_id)?;
let ipaddr = IpAddr::by_id(db, self.ip_addr_id)?;
Ok(PrintableSubdomainIpAddr {
subdomain: subdomain.value.to_string(),
subdomain: subdomain.value,
ipaddr: ipaddr.value.parse()?,
})
}
@@ -126,7 +124,7 @@ impl Printable<PrintableSubdomainIpAddr> for NewSubdomainIpAddr {
let subdomain = Subdomain::by_id(db, self.subdomain_id)?;
let ipaddr = IpAddr::by_id(db, self.ip_addr_id)?;
Ok(PrintableSubdomainIpAddr {
subdomain: subdomain.value.to_string(),
subdomain: subdomain.value,
ipaddr: ipaddr.value.parse()?,
})
}

View File

@@ -3,10 +3,7 @@ use crate::errors::*;
use crate::fmt::Write;
use crate::fmt::colors::*;
use crate::models::*;
use diesel;
use diesel::prelude::*;
use crate::url;
#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)]
#[belongs_to(Subdomain)]
@@ -292,7 +289,7 @@ impl InsertToNew for InsertUrl {
type Target = NewUrl;
fn try_into_new(self) -> Result<NewUrl> {
let url = url::Url::parse(&self.value)?;
let url = ::url::Url::parse(&self.value)?;
let path = url.path().to_string();
let redirect = if let Some(redirect) = self.redirect {
@@ -354,12 +351,12 @@ impl Updateable<Url> for UrlChangeset {
Self::clear_if_equal(&mut self.redirect, &existing.redirect);
}
fn fmt(&self, updates: &mut Vec<String>) {
Self::push_value(updates, "online", &self.online);
Self::push_value(updates, "status", &self.status);
Self::push_raw(updates, "body", self.body.as_ref().map(|x| format!("[{} bytes]", x.len())));
Self::push_value(updates, "title", &self.title);
Self::push_value(updates, "redirect", &self.redirect);
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
Self::push_value(updates, "online", &self.online, colors);
Self::push_value(updates, "status", &self.status, colors);
Self::push_raw(updates, "body", self.body.as_ref().map(|x| format!("[{} bytes]", x.len())), colors);
Self::push_value(updates, "title", &self.title, colors);
Self::push_value(updates, "redirect", &self.redirect, colors);
}
}

243
src/notify/mod.rs Normal file
View File

@@ -0,0 +1,243 @@
mod rules;
use crate::errors::*;
use crate::cmd::run_cmd::prepare_keyring;
use crate::cmd::run_cmd::Params;
use crate::engine::Module;
use crate::options;
use crate::shell::Shell;
use crate::term::SpinLogger;
use crate::worker;
use self::rules::Glob;
use sn0int_common::metadata::Source;
use sn0int_std::blobs::Blob;
use sn0int_std::ratelimits::Ratelimiter;
use std::collections::HashMap;
#[derive(Debug, StructOpt, Serialize)]
pub struct Notification {
pub subject: String,
pub body: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NotificationConfig {
#[serde(default)]
pub workspaces: Vec<String>,
#[serde(default)]
pub topics: Vec<Glob>,
pub script: String,
#[serde(default)]
pub options: Vec<options::Opt>,
}
fn apply_rule<T>(name: &str, filters: &[T], value: &str, cmp: fn(&T, &str) -> Option<bool>) -> bool {
if !filters.is_empty() {
debug!("{} filter is active", name);
for filter in filters {
match cmp(filter, value) {
Some(true) => {
debug!("{} was allow-listed", name);
return true;
}
Some(false) => {
debug!("{} was excluded", name);
return false;
}
_ => (),
}
}
debug!("{} didn't match any rules, skipping", name);
false
} else {
true
}
}
impl NotificationConfig {
fn matches(&self, name: &str, workspace: &str, topic: &str) -> bool {
debug!("Testing notification with rules: {:?}", name);
if !apply_rule("workspace", &self.workspaces, workspace, |filter, value| if filter == value { Some(true) } else { None }) {
return false;
}
if !apply_rule("topic", &self.topics, topic, |filter, value| filter.matches(value)) {
return false;
}
debug!("Notification matches this config");
true
}
}
pub fn trigger_notify_event<T: SpinLogger>(rl: &mut Shell, spinner: &mut T, ratelimit: &mut Ratelimiter, topic: &str, notification: &Notification) -> Result<()> {
run_router(rl, spinner, ratelimit, false, topic, notification)
}
fn prepare_arg(notification: &Notification) -> Result<(serde_json::Value, Option<String>, Vec<Blob>)> {
let arg = serde_json::to_value(notification)?;
Ok((arg, None, vec![]))
}
pub fn exec(rl: &mut Shell, module: &Module, ratelimit: &mut Ratelimiter, options: HashMap<String, String>, verbose: u64, notification: &Notification) -> Result<usize> {
let module_name = module.canonical();
debug!("Setting up notification execution with {:?}", module_name);
if *module.source() != Some(Source::Notifications) {
bail!("Module doesn't take notifications as source");
}
let params = Params {
threads: 1,
verbose,
stdin: false,
grants: &[],
grant_full_keyring: false,
deny_keyring: false,
exit_on_error: false,
};
prepare_keyring(rl.keyring_mut(), &module, &params)?;
let args = vec![prepare_arg(&notification)?];
debug!("Executing notification module {:?}", module_name);
let errors = worker::spawn(rl, &module, ratelimit, args, &params, rl.config().network.proxy, options);
debug!("Notification module {:?} exited with {:?} errors", module_name, errors);
Ok(errors)
}
pub fn run_router<T: SpinLogger>(rl: &mut Shell, spinner: &mut T, ratelimit: &mut Ratelimiter, dry_run: bool, topic: &str, notification: &Notification) -> Result<()> {
let configs = rl.config().notifications.clone();
debug!("Running notification router");
for (name, config) in configs {
if rl.signal_register().ctrlc_received() {
debug!("Exiting notification router due to ctrl-c");
break;
}
if config.matches(&name, rl.workspace(), topic) {
let module = rl.library().get(&config.script)?.clone();
if dry_run {
spinner.success(&format!("Executed {} for {:?} (dry-run)", module.canonical(), name));
} else {
let options = options::Opt::collect(&config.options);
match exec(rl, &module, ratelimit, options, 0, notification) {
Ok(0) => {
let msg = format!("Executed {} for {:?}", module.canonical(), name);
spinner.success(&msg);
},
Ok(errors) => {
let msg = format!("Executed {} for {:?} ({} errors)", module.canonical(), name, errors);
spinner.error(&msg);
},
Err(err) => {
spinner.error(&format!("Fatal {} for {:?}: {}", module.canonical(), name, err));
},
}
}
}
}
debug!("Notification router finished");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn mkconfig(topics: &[&str]) -> NotificationConfig {
let topics = topics.iter()
.map(|s| s.parse::<Glob>())
.collect::<Result<Vec<_>>>().unwrap();
NotificationConfig {
workspaces: Vec::new(),
topics,
script: "some/script".to_string(),
options: Vec::new(),
}
}
#[test]
fn test_empty_topic() {
let config = mkconfig(&[]);
assert!(config.matches("name", "workspace", "topic"));
}
#[test]
fn test_match_topic() {
let config = mkconfig(&[
"db:subdomain:example.com:*",
]);
assert!(config.matches("name", "workspace", "db:subdomain:example.com:update"));
}
#[test]
fn test_not_match_topic() {
let config = mkconfig(&[
"db:subdomain:example.com:*",
]);
assert!(!config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
#[test]
fn test_exclude_topic() {
let config = mkconfig(&[
"!db:subdomain:example.com:*",
]);
assert!(!config.matches("name", "workspace", "db:subdomain:example.com:update"));
}
#[test]
fn test_exclude_other_topic() {
let config = mkconfig(&[
"!db:subdomain:example.com:*",
"db:subdomain:foobar.com:*",
]);
assert!(config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
#[test]
fn test_no_inverse_does_not_imply_match() {
let config = mkconfig(&[
"!db:subdomain:example.com:*",
]);
assert!(!config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
#[test]
fn test_everything_except() {
let config = mkconfig(&[
"!db:subdomain:example.com:*",
"*:*:*:*",
]);
assert!(config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
#[test]
fn test_exclude_everything() {
let config = mkconfig(&[
"!*:*:*:*",
]);
assert!(!config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
#[test]
fn test_execute_in_order_1() {
let config = mkconfig(&[
"!*:*:*:*",
"*:*:*:*",
]);
assert!(!config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
#[test]
fn test_execute_in_order_2() {
let config = mkconfig(&[
"*:*:*:*",
"!*:*:*:*",
]);
assert!(config.matches("name", "workspace", "db:subdomain:foobar.com:update"));
}
}

138
src/notify/rules.rs Normal file
View File

@@ -0,0 +1,138 @@
use crate::errors::*;
use serde::de::{self, Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
use std::result;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct Glob {
patterns: Vec<glob::Pattern>,
src: String,
inverse: bool,
}
impl Glob {
pub fn matches(&self, topic: &str) -> Option<bool> {
let mut filter = self.patterns.iter();
let mut topic = topic.split(':');
loop {
match (filter.next(), topic.next()) {
(Some(filter), Some(topic)) => if !filter.matches(&topic) {
return None;
},
(None, None) => return Some(!self.inverse),
(_, _) => return None,
}
}
}
}
impl FromStr for Glob {
type Err = Error;
fn from_str(s: &str) -> Result<Glob> {
let (s, inverse) = if let Some(stripped) = s.strip_prefix('!') {
(stripped, true)
} else {
(s, false)
};
let patterns = s.split(':')
.map(|s| glob::Pattern::new(s).map_err(Error::from))
.collect::<Result<Vec<_>>>()?;
Ok(Glob {
patterns,
src: s.to_string(),
inverse,
})
}
}
impl Serialize for Glob {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where S: Serializer
{
serializer.serialize_str(&self.src)
}
}
impl<'de> Deserialize<'de> for Glob {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
where D: Deserializer<'de>
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn match_topic_str(filter: &str, value: &str) -> bool {
let filter: Glob = filter.parse().unwrap();
filter.matches(value).unwrap_or(false)
}
#[test]
fn test_match_topic_exact() {
assert!(match_topic_str("topic:hello-world", "topic:hello-world"));
}
#[test]
fn test_match_topic_starts_with() {
assert!(match_topic_str("topic:*", "topic:hello-world"));
}
#[test]
fn test_match_topic_ends_with() {
assert!(match_topic_str("*:hello-world", "topic:hello-world"));
}
#[test]
fn test_match_topic_one_wildcard_one_section() {
assert!(match_topic_str("a:*:z", "a:b:z"));
}
#[test]
fn test_match_topic_one_wildcard_not_two_sections() {
assert!(!match_topic_str("a:*:z", "a:b:c:z"));
}
#[test]
fn test_match_topic_two_wildcards_two_sections() {
assert!(match_topic_str("a:*:*:z", "a:b:c:z"));
}
#[test]
fn test_match_topic_one_wildcard_not_two_sections_start() {
assert!(!match_topic_str("a:*", "a:b:c"));
}
#[test]
fn test_match_topic_one_wildcard_not_two_sections_end() {
assert!(!match_topic_str("*:z", "b:c:z"));
}
#[test]
fn test_match_topic_many_wildcards() {
assert!(match_topic_str("a:*:*:d:e:*:g:*:z", "a:b:c:d:e:f:g:h:z"));
}
#[test]
fn test_match_topic_empty_filter() {
assert!(!match_topic_str("", "abc"));
}
#[test]
fn test_inverse_match() {
assert!(!match_topic_str("!topic:*", "topic:hello-world"));
}
#[test]
fn test_no_inverse_match() {
// Just because an inverse rule doesn't match this doesn't imply a match
assert!(!match_topic_str("!foo:*", "topic:hello-world"));
}
}

View File

@@ -1,9 +1,11 @@
use crate::errors::*;
use serde::de::{self, Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
use std::collections::HashMap;
use std::result;
use std::str::FromStr;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Opt {
key: String,
value: String,
@@ -32,3 +34,26 @@ impl FromStr for Opt {
}
}
}
impl ToString for Opt {
fn to_string(&self) -> String {
format!("{}={}", self.key, self.value)
}
}
impl Serialize for Opt {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where S: Serializer
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Opt {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
where D: Deserializer<'de>
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}

View File

@@ -1,14 +1,11 @@
use crate::config::Config;
use crate::errors::*;
use crate::workspaces::Workspace;
use dirs;
use std::fs;
use std::path::PathBuf;
pub fn sn0int_dir() -> Result<PathBuf> {
let path = dirs::data_dir()
let path = dirs_next::data_dir()
.ok_or_else(|| format_err!("Failed to find data directory"))?;
let path = path.join("sn0int");
fs::create_dir_all(&path)
@@ -56,7 +53,7 @@ pub fn blobs_dir(workspace: &Workspace) -> Result<PathBuf> {
}
pub fn cache_dir() -> Result<PathBuf> {
let path = dirs::cache_dir()
let path = dirs_next::cache_dir()
.ok_or_else(|| format_err!("Failed to find cache directory"))?;
let path = path.join("sn0int");
fs::create_dir_all(&path)

View File

@@ -4,11 +4,11 @@ use crate::api::Client;
use crate::auth;
use crate::config::Config;
use crate::engine::{Library, Module};
use colored::Colorize;
use colored::{Color, Colorize};
use separator::Separatable;
use std::fmt::Write;
use sn0int_common::ModuleID;
use sn0int_common::api::ModuleInfoResponse;
use sn0int_common::metadata::Metadata;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -54,7 +54,7 @@ impl Updater {
fs::write(&path, module.code)
.context(format_err!("Failed to write to {:?}", path))?;
Ok(version.to_string())
Ok(version)
} else {
let infos = self.query_module(&install.module)
.context("Failed to query module infos")?;
@@ -112,13 +112,11 @@ pub fn run_publish(_args: &Args, publish: &Publish, config: &Config) -> Result<(
let name = name.to_os_string().into_string()
.map_err(|_| format_err!("Failed to decode file name"))?;
let code = fs::read_to_string(path)
.context("Failed to read module")?;
let metadata = code.parse::<Metadata>()?;
let module = Module::load(path, "anonymous", &name, false)?;
let label = format!("Uploading {} {} ({:?})", name, metadata.version, path);
let label = format!("Uploading {} {} ({:?})", name, module.version(), path);
match worker::spawn_fn(&label, || {
client.publish_module(&name, code.to_string())
client.publish_module(&name, module.code().to_string())
}, true) {
Ok(result) => term::info(&format!("Published {}/{} {} ({:?})",
result.author,
@@ -127,7 +125,7 @@ pub fn run_publish(_args: &Args, publish: &Publish, config: &Config) -> Result<(
path)),
Err(err) => term::error(&format!("Failed to publish {} {} ({:?}): {}",
name,
metadata.version,
module.version(),
path,
err)),
}
@@ -242,6 +240,12 @@ impl Task for UpdateTask {
}
}
#[inline]
fn write_tag(out: &mut String, color: Color, txt: &str) -> Result<()> {
write!(out, " [{}]", txt.color(color))?;
Ok(())
}
pub fn run_search(library: &Library, search: &Search, config: &Config) -> Result<()> {
let client = Client::new(&config)?;
@@ -258,12 +262,23 @@ pub fn run_search(library: &Library, search: &Search, config: &Config) -> Result
continue;
}
println!("{} ({}) - {} downloads{}{}", canonical.green(),
module.latest.yellow(),
module.downloads.separated_string(),
(if module.featured { " [featured]" } else { "" }).cyan(),
(if installed.is_some() { " [installed]" } else { "" }).green());
println!("\t{}", module.description);
let mut out = format!("{}/{} {} - {} downloads",
module.author.purple(),
module.name,
module.latest.blue(),
module.downloads.separated_string(),
);
if module.featured {
write_tag(&mut out, Color::Cyan, "featured")?;
}
if installed.is_some() {
write_tag(&mut out, Color::Green, "installed")?;
}
println!("{}", out.bold());
println!(" {}", module.description);
}
Ok(())

View File

@@ -55,6 +55,8 @@ impl Highlighter for ReplCompleter {
}
impl Hinter for ReplCompleter {
type Hint = String;
#[inline]
fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<String> {
None

View File

@@ -102,7 +102,7 @@ pub fn run(config: &Config) -> Result<()> {
asn,
};
let tx = DummyIpcChild::new();
let tx = DummyIpcChild::create();
let (lua, state) = ctx::ctx(env, tx);
let mut repl = Repl::new(lua, state);

View File

@@ -1,6 +1,5 @@
use crate::errors::*;
use serde;
use crate::db::Family;
use crate::engine::ctx::State;
use crate::engine::structs;
@@ -199,65 +198,63 @@ fn gen_changeset2<T: Model, U: UpdateToChangeset<C>, C: Updateable<T>>(object: L
Ok((existing.id(), value, update))
}
fn run_update(state: Arc<dyn State>, family: &str, object: AnyLuaValue, update: AnyLuaValue) -> Result<Option<i32>> {
let family = Family::from_str(&family)?;
let object = LuaJsonValue::from(object);
let update = LuaJsonValue::from(update);
let (id, value, update) = match family {
Family::Domain => bail!("Domain doesn't have mutable fields"),
Family::Subdomain => gen_changeset::<Subdomain, SubdomainUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Subdomain(u))),
Family::Ipaddr => gen_changeset::<IpAddr, IpAddrUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::IpAddr(u))),
Family::SubdomainIpaddr => bail!("Subdomain-IpAddr doesn't have mutable fields"),
Family::Url => gen_changeset2::<Url, UrlUpdate, UrlChangeset>(object, update)
.map(|(id, v, u)| (id, v, Update::Url(u))),
Family::Email => gen_changeset::<Email, EmailUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Email(u))),
Family::Phonenumber => gen_changeset::<PhoneNumber, PhoneNumberUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::PhoneNumber(u))),
Family::Device => gen_changeset::<Device, DeviceUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Device(u))),
Family::Network => gen_changeset::<Network, NetworkUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Network(u))),
Family::NetworkDevice => gen_changeset::<NetworkDevice, NetworkDeviceUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::NetworkDevice(u))),
Family::Account => gen_changeset::<Account, AccountUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Account(u))),
Family::Breach => bail!("Breach doesn't have mutable fields"),
Family::BreachEmail => gen_changeset::<BreachEmail, BreachEmailUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::BreachEmail(u))),
Family::Image => gen_changeset::<Image, ImageUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Image(u))),
Family::Port => gen_changeset::<Port, PortUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Port(u))),
Family::Netblock => gen_changeset::<Netblock, NetblockUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Netblock(u))),
Family::Cryptoaddr => gen_changeset::<CryptoAddr, CryptoAddrUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::CryptoAddr(u))),
}?;
if update.is_dirty() {
let r = match state.db_update(family, value, update)? {
// Inserted and Found are technically unreachable
DatabaseResponse::Inserted(id) => Some(id),
DatabaseResponse::Updated(id) => Some(id),
DatabaseResponse::NoChange(id) => Some(id),
DatabaseResponse::Found(id) => Some(id),
DatabaseResponse::None => None,
};
Ok(r)
} else {
Ok(Some(id))
}
}
pub fn db_update(lua: &mut hlua::Lua, state: Arc<dyn State>) {
lua.set("db_update", hlua::function3(move |family: String, object: AnyLuaValue, update: AnyLuaValue| -> Result<Option<i32>> {
let family = Family::from_str(&family)
.map_err(|e| state.set_error(e.into()))?;
let object = LuaJsonValue::from(object);
let update = LuaJsonValue::from(update);
let update = match family {
Family::Domain => bail!("Domain doesn't have mutable fields"),
Family::Subdomain => gen_changeset::<Subdomain, SubdomainUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Subdomain(u))),
Family::Ipaddr => gen_changeset::<IpAddr, IpAddrUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::IpAddr(u))),
Family::SubdomainIpaddr => bail!("Subdomain-IpAddr doesn't have mutable fields"),
Family::Url => gen_changeset2::<Url, UrlUpdate, UrlChangeset>(object, update)
.map(|(id, v, u)| (id, v, Update::Url(u))),
Family::Email => gen_changeset::<Email, EmailUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Email(u))),
Family::Phonenumber => gen_changeset::<PhoneNumber, PhoneNumberUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::PhoneNumber(u))),
Family::Device => gen_changeset::<Device, DeviceUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Device(u))),
Family::Network => gen_changeset::<Network, NetworkUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Network(u))),
Family::NetworkDevice => gen_changeset::<NetworkDevice, NetworkDeviceUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::NetworkDevice(u))),
Family::Account => gen_changeset::<Account, AccountUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Account(u))),
Family::Breach => bail!("Breach doesn't have mutable fields"),
Family::BreachEmail => gen_changeset::<BreachEmail, BreachEmailUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::BreachEmail(u))),
Family::Image => gen_changeset::<Image, ImageUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Image(u))),
Family::Port => gen_changeset::<Port, PortUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Port(u))),
Family::Netblock => gen_changeset::<Netblock, NetblockUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Netblock(u))),
Family::Cryptoaddr => gen_changeset::<CryptoAddr, CryptoAddrUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::CryptoAddr(u))),
};
let (id, value, update) = update
.map_err(|e| state.set_error(e))?;
if update.is_dirty() {
let r = state.db_update(value, update)
.map_err(|e| state.set_error(e))?;
let r = match r {
// Inserted and Found are technically unreachable
DatabaseResponse::Inserted(id) => Some(id),
DatabaseResponse::Updated(id) => Some(id),
DatabaseResponse::NoChange(id) => Some(id),
DatabaseResponse::Found(id) => Some(id),
DatabaseResponse::None => None,
};
Ok(r)
} else {
Ok(Some(id))
}
run_update(state.clone(), &family, object, update)
.map_err(|e| state.set_error(e))
}))
}

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