Repository: kpcyrd/sn0int (language: rust)

Issue:
Cryptocurrency addresses
It would be useful if we could add crypto currency addresses to our scope, link them to a person and also check the balance.

This could be somewhat tricky because I'm not sure if the address does identify an entity uniquely. We might have to set the unique constraint on `currency + address` instead of just an address.


Discussion / hints:

Fix patch:
diff --git a/migrations/2019-11-19-154056_cryptoaddr/down.sql b/migrations/2019-11-19-154056_cryptoaddr/down.sql
new file mode 100644
index 00000000..9aba1dfd
--- /dev/null
+++ b/migrations/2019-11-19-154056_cryptoaddr/down.sql
@@ -0,0 +1,1 @@
+DROP TABLE cryptoaddrs;
diff --git a/migrations/2019-11-19-154056_cryptoaddr/up.sql b/migrations/2019-11-19-154056_cryptoaddr/up.sql
new file mode 100644
index 00000000..3d7b1cab
--- /dev/null
+++ b/migrations/2019-11-19-154056_cryptoaddr/up.sql
@@ -0,0 +1,13 @@
+CREATE TABLE cryptoaddrs (
+    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+    value VARCHAR NOT NULL,
+    currency VARCHAR,
+    denominator INTEGER,
+    balance INTEGER,
+    received INTEGER,
+    first_seen DATETIME,
+    last_withdrawal DATETIME,
+    unscoped BOOLEAN DEFAULT 0 NOT NULL,
+    description VARCHAR,
+    CONSTRAINT netblock_unique UNIQUE (value)
+);
diff --git a/sn0int-registry/sn0int-common/src/metadata.rs b/sn0int-registry/sn0int-common/src/metadata.rs
index 47163c56..37b129c7 100644
--- a/sn0int-registry/sn0int-common/src/metadata.rs
+++ b/sn0int-registry/sn0int-common/src/metadata.rs
@@ -42,6 +42,7 @@ pub enum Source {
     Images,
     Ports,
     Netblocks,
+    CryptoAddrs(Option<String>),
     KeyRing(String),
 }
 
@@ -61,6 +62,7 @@ impl Source {
             Source::Images => "images",
             Source::Ports => "ports",
             Source::Netblocks => "netblocks",
+            Source::CryptoAddrs(_) => "cryptoaddrs",
             Source::KeyRing(_) => "keyring",
         }
     }
@@ -91,6 +93,7 @@ impl FromStr for Source {
             ("images", None) => Ok(Source::Images),
             ("ports", None) => Ok(Source::Ports),
             ("netblocks", None) => Ok(Source::Netblocks),
+            ("cryptoaddrs", param) => Ok(Source::CryptoAddrs(param.map(String::from))),
             ("keyring", Some(param)) => Ok(Source::KeyRing(param.to_string())),
             (x, Some(param)) => bail!("Unknown Source: {:?} ({:?})", x, param),
             (x, None) => bail!("Unknown Source: {:?}", x),
diff --git a/src/cmd/add_cmd.rs b/src/cmd/add_cmd.rs
index 2a4f3da7..14656a99 100644
--- a/src/cmd/add_cmd.rs
+++ b/src/cmd/add_cmd.rs
@@ -33,9 +33,6 @@ pub enum Target {
     /// Insert subdomain into the database
     #[structopt(name="subdomain")]
     Subdomain(AddSubdomain),
-    /// Insert ip network into the database
-    #[structopt(name="netblock")]
-    Netblock(AddNetblock),
     /// Insert ip address into the database
     #[structopt(name="ipaddr")]
     IpAddr(AddIpAddr),
@@ -63,6 +60,12 @@ pub enum Target {
     /// Insert images into the database
     #[structopt(name="image")]
     Image(AddImage),
+    /// Insert ip network into the database
+    #[structopt(name="netblock")]
+    Netblock(AddNetblock),
+    /// Insert a crypto currency address into the database
+    #[structopt(name="cryptoaddr")]
+    CryptoAddr(AddCryptoAddr),
 }
 
 impl Cmd for Args {
@@ -80,6 +83,7 @@ impl Cmd for Args {
             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::CryptoAddr(args) => args.insert(rl, self.dry_run),
         }
     }
 }
@@ -550,3 +554,30 @@ impl IntoInsert for AddNetblock {
         }))
     }
 }
+
+#[derive(Debug, StructOpt)]
+pub struct AddCryptoAddr {
+    address: Option<String>,
+}
+
+impl IntoInsert for AddCryptoAddr {
+    fn into_insert(self, _rl: &mut Shell) -> Result<Insert> {
+        let address = if let Some(address) = self.address {
+            address
+        } else {
+            utils::question("Address")?
+        };
+
+        Ok(Insert::CryptoAddr(NewCryptoAddr {
+            value: address,
+            currency: None,
+            denominator: None,
+            balance: None,
+            received: None,
+            first_seen: None,
+            last_withdrawal: None,
+            unscoped: false,
+            description: None,
+        }))
+    }
+}
diff --git a/src/cmd/delete_cmd.rs b/src/cmd/delete_cmd.rs
index 314422d0..08c8d757 100644
--- a/src/cmd/delete_cmd.rs
+++ b/src/cmd/delete_cmd.rs
@@ -32,6 +32,7 @@ impl Cmd for Args {
             Target::Images(filter) => delete::<Image>(rl, &filter),
             Target::Ports(filter) => delete::<Port>(rl, &filter),
             Target::Netblocks(filter) => delete::<Netblock>(rl, &filter),
+            Target::CryptoAddrs(filter) => delete::<CryptoAddr>(rl, &filter),
         }?;
         term::info(&format!("Deleted {} rows", rows));
         Ok(())
diff --git a/src/cmd/noscope_cmd.rs b/src/cmd/noscope_cmd.rs
index b49268d8..5ead7154 100644
--- a/src/cmd/noscope_cmd.rs
+++ b/src/cmd/noscope_cmd.rs
@@ -32,6 +32,7 @@ impl Cmd for Args {
             Target::Images(filter) => noscope::<Image>(rl, &filter),
             Target::Ports(filter) => noscope::<Port>(rl, &filter),
             Target::Netblocks(filter) => noscope::<Netblock>(rl, &filter),
+            Target::CryptoAddrs(filter) => noscope::<CryptoAddr>(rl, &filter),
         }?;
         term::info(&format!("Updated {} rows", rows));
         Ok(())
diff --git a/src/cmd/run_cmd.rs b/src/cmd/run_cmd.rs
index 62e25528..bcb28de8 100644
--- a/src/cmd/run_cmd.rs
+++ b/src/cmd/run_cmd.rs
@@ -136,6 +136,7 @@ pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>)
         Some(Source::Images) => prepare_args::<Image>(rl, &filter, None),
         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::KeyRing(namespace)) => {
             let keyring = rl.keyring();
             if keyring.is_access_granted(&module, &namespace) {
diff --git a/src/cmd/scope_cmd.rs b/src/cmd/scope_cmd.rs
index d9f75316..fa86fd6e 100644
--- a/src/cmd/scope_cmd.rs
+++ b/src/cmd/scope_cmd.rs
@@ -32,6 +32,7 @@ impl Cmd for Args {
             Target::Images(filter) => scope::<Image>(rl, &filter),
             Target::Ports(filter) => scope::<Port>(rl, &filter),
             Target::Netblocks(filter) => scope::<Netblock>(rl, &filter),
+            Target::CryptoAddrs(filter) => scope::<CryptoAddr>(rl, &filter),
         }?;
         term::info(&format!("Updated {} rows", rows));
         Ok(())
diff --git a/src/cmd/select_cmd.rs b/src/cmd/select_cmd.rs
index 3a94e430..69cfa8ed 100644
--- a/src/cmd/select_cmd.rs
+++ b/src/cmd/select_cmd.rs
@@ -97,6 +97,7 @@ impl Cmd for Args {
             Target::Images(filter) => printer.select::<Image>(&filter),
             Target::Ports(filter) => printer.select::<Port>(&filter),
             Target::Netblocks(filter) => printer.select::<Netblock>(&filter),
+            Target::CryptoAddrs(filter) => printer.select::<CryptoAddr>(&filter),
         }
     }
 }
diff --git a/src/cmd/target_cmd.rs b/src/cmd/target_cmd.rs
index 82b36358..5fe1bacb 100644
--- a/src/cmd/target_cmd.rs
+++ b/src/cmd/target_cmd.rs
@@ -43,6 +43,7 @@ pub fn run(rl: &mut Shell, args: &[String]) -> Result<()> {
             Source::Images => select::<Image>(rl, None)?,
             Source::Ports => select::<Port>(rl, None)?,
             Source::Netblocks => select::<Netblock>(rl, None)?,
+            Source::CryptoAddrs(currency) => select::<CryptoAddr>(rl, currency.as_ref())?,
             Source::KeyRing(namespace) => {
                 for key in rl.keyring().list_for(&namespace) {
                     println!("{}:{}", key.namespace, key.name);
@@ -77,6 +78,7 @@ fn count_selected(rl: &mut Shell, source: &Source) -> Result<usize> {
         Source::Images => db.filter::<Image>(&filter)?.len(),
         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::KeyRing(namespace) => rl.keyring().list_for(&namespace).len(),
     };
     Ok(num)
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 44842471..6dd760e6 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -52,6 +52,7 @@ pub enum Family {
     Image,
     Port,
     Netblock,
+    Cryptoaddr,
 }
 
 impl Family {
@@ -80,6 +81,7 @@ pub enum Table {
     Images,
     Ports,
     Netblocks,
+    Cryptoaddrs,
 }
 
 impl Table {
@@ -179,6 +181,7 @@ impl Database {
             Insert::Image(object) => self.insert_struct(object, scoped),
             Insert::Port(object) => self.insert_struct(object, scoped),
             Insert::Netblock(object) => self.insert_struct(object, scoped),
+            Insert::CryptoAddr(object) => self.insert_struct(object, scoped),
         }
     }
 
@@ -268,6 +271,7 @@ impl Database {
             Update::Image(update) => self.update_image(update),
             Update::Port(update) => self.update_port(update),
             Update::Netblock(update) => self.update_netblock(update),
+            Update::CryptoAddr(update) => self.update_cryptoaddr(update),
         }
     }
 
@@ -375,6 +379,14 @@ impl Database {
         Ok(netblock_update.id)
     }
 
+    pub fn update_cryptoaddr(&self, cryptoaddr_update: &CryptoAddrUpdate) -> Result<i32> {
+        use crate::schema::cryptoaddrs::columns::*;
+        diesel::update(cryptoaddrs::table.filter(id.eq(cryptoaddr_update.id)))
+            .set(cryptoaddr_update)
+            .execute(&self.db)?;
+        Ok(cryptoaddr_update.id)
+    }
+
     fn get_opt_typed<T: Model + Scopable>(&self, value: &T::ID) -> Result<Option<i32>> {
         match T::get_opt(self, &value)? {
             Some(ref obj) if obj.scoped() => Ok(Some(obj.id())),
@@ -400,6 +412,7 @@ impl Database {
             Family::Image => self.get_opt_typed::<Image>(&value),
             Family::Port => self.get_opt_typed::<Port>(&value),
             Family::Netblock => self.get_opt_typed::<Netblock>(&value),
+            Family::Cryptoaddr => self.get_opt_typed::<CryptoAddr>(&value),
         }
     }
 
diff --git a/src/db/ttl.rs b/src/db/ttl.rs
index f02b10b1..53bb7620 100644
--- a/src/db/ttl.rs
+++ b/src/db/ttl.rs
@@ -117,6 +117,7 @@ impl Ttl {
             Table::Images => Image::delete_id(db, self.key)?,
             Table::Ports => Port::delete_id(db, self.key)?,
             Table::Netblocks => Netblock::delete_id(db, self.key)?,
+            Table::Cryptoaddrs => CryptoAddr::delete_id(db, self.key)?,
         };
 
         diesel::delete(self)
diff --git a/src/engine/ctx.rs b/src/engine/ctx.rs
index 36111a26..4eff3fc0 100644
--- a/src/engine/ctx.rs
+++ b/src/engine/ctx.rs
@@ -429,6 +429,7 @@ pub fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<dyn Reporter>>>) -> (hlua
     runtime::img_load(&mut lua, state.clone());
     runtime::img_nudity(&mut lua, state.clone());
     runtime::info(&mut lua, state.clone());
+    runtime::intval(&mut lua, state.clone());
     runtime::json_decode(&mut lua, state.clone());
     runtime::json_decode_stream(&mut lua, state.clone());
     runtime::json_encode(&mut lua, state.clone());
diff --git a/src/filters.rs b/src/filters.rs
index 7bab7872..24c753c2 100644
--- a/src/filters.rs
+++ b/src/filters.rs
@@ -45,6 +45,9 @@ pub enum Target {
     /// On ipnets
     #[structopt(name="netblocks")]
     Netblocks(Filter),
+    /// On crypto currency addresses
+    #[structopt(name="cryptoaddrs")]
+    CryptoAddrs(Filter),
 }
 
 #[derive(Debug, StructOpt)]
diff --git a/src/models/cryptoaddr.rs b/src/models/cryptoaddr.rs
new file mode 100644
index 00000000..2ef80546
--- /dev/null
+++ b/src/models/cryptoaddr.rs
@@ -0,0 +1,427 @@
+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 {
+    pub id: i32,
+    pub value: String,
+    pub currency: Option<String>,
+    pub denominator: Option<i32>,
+    pub balance: Option<i64>,
+    pub received: Option<i64>,
+    pub first_seen: Option<NaiveDateTime>,
+    pub last_withdrawal: Option<NaiveDateTime>,
+    pub unscoped: bool,
+    pub description: Option<String>,
+}
+
+impl Model for CryptoAddr {
+    type ID = str;
+
+    fn to_string(&self) -> String {
+        self.value.to_owned()
+    }
+
+    fn list(db: &Database) -> Result<Vec<Self>> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        let results = cryptoaddrs.load::<Self>(db.db())?;
+
+        Ok(results)
+    }
+
+    fn filter(db: &Database, filter: &Filter) -> Result<Vec<Self>> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        let query = cryptoaddrs.filter(filter.sql());
+        let results = query.load::<Self>(db.db())?;
+
+        Ok(results)
+    }
+
+    fn filter_with_param(db: &Database, filter: &Filter, param: &str) -> Result<Vec<Self>> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        let query = cryptoaddrs
+            .filter(currency.eq(param))
+            .filter(filter.sql());
+        let results = query.load::<Self>(db.db())?;
+
+        Ok(results)
+    }
+
+    fn delete(db: &Database, filter: &Filter) -> Result<usize> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        diesel::delete(cryptoaddrs.filter(filter.sql()))
+            .execute(db.db())
+            .map_err(Error::from)
+    }
+
+    fn delete_id(db: &Database, my_id: i32) -> Result<usize> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        diesel::delete(cryptoaddrs.filter(id.eq(my_id)))
+            .execute(db.db())
+            .map_err(Error::from)
+    }
+
+    fn id(&self) -> i32 {
+        self.id
+    }
+
+    fn value(&self) -> &Self::ID {
+        &self.value
+    }
+
+    fn by_id(db: &Database, my_id: i32) -> Result<Self> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        let domain = cryptoaddrs.filter(id.eq(my_id))
+            .first::<Self>(db.db())?;
+
+        Ok(domain)
+    }
+
+    fn get(db: &Database, query: &Self::ID) -> Result<Self> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        let netblock = cryptoaddrs.filter(value.eq(query))
+            .first::<Self>(db.db())?;
+
+        Ok(netblock)
+    }
+
+    fn get_opt(db: &Database, query: &Self::ID) -> Result<Option<Self>> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        let netblock = cryptoaddrs.filter(value.eq(query))
+            .first::<Self>(db.db())
+            .optional()?;
+
+        Ok(netblock)
+    }
+}
+
+impl Scopable for CryptoAddr {
+    fn scoped(&self) -> bool {
+        !self.unscoped
+    }
+
+    fn scope(db: &Database, filter: &Filter) -> Result<usize> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        diesel::update(cryptoaddrs.filter(filter.sql()))
+            .set(unscoped.eq(false))
+            .execute(db.db())
+            .map_err(Error::from)
+    }
+
+    fn noscope(db: &Database, filter: &Filter) -> Result<usize> {
+        use crate::schema::cryptoaddrs::dsl::*;
+
+        diesel::update(cryptoaddrs.filter(filter.sql()))
+            .set(unscoped.eq(true))
+            .execute(db.db())
+            .map_err(Error::from)
+    }
+}
+
+pub struct PrintableCryptoAddr {
+    value: String,
+}
+
+impl fmt::Display for PrintableCryptoAddr {
+    fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
+        write!(w, "{:?}", self.value)
+    }
+}
+
+impl Printable<PrintableCryptoAddr> for CryptoAddr {
+    fn printable(&self, _db: &Database) -> Result<PrintableCryptoAddr> {
+        Ok(PrintableCryptoAddr {
+            value: self.value.to_string(),
+        })
+    }
+}
+
+pub struct DetailedCryptoAddr {
+    id: i32,
+    value: String,
+    currency: Option<String>,
+    denominator: Option<i32>,
+    balance: Option<i64>,
+    received: Option<i64>,
+    first_seen: Option<NaiveDateTime>,
+    last_withdrawal: Option<NaiveDateTime>,
+    unscoped: bool,
+    description: Option<String>,
+}
+
+#[inline]
+fn add_currency(w: &mut fmt::DetailFormatter, num: &Option<i64>, denominator: &Option<i32>) -> fmt::Result {
+    if let Some(&num) = num.as_ref() {
+        let denominator = denominator.unwrap_or(0);
+        let display = display_currency(num as u64, denominator as usize);
+        if num > 0 {
+            w.display::<Green, _>(display)?;
+        } else {
+            w.display::<Red, _>(display)?;
+        }
+    }
+    Ok(())
+}
+
+impl DisplayableDetailed for DetailedCryptoAddr {
+    #[inline]
+    fn scoped(&self) -> bool {
+        !self.unscoped
+    }
+
+    #[inline]
+    fn print(&self, w: &mut fmt::DetailFormatter) -> fmt::Result {
+        w.id(self.id)?;
+        w.debug::<Green, _>(&self.value)?;
+
+        w.start_group();
+        w.opt_debug::<Yellow, _>(&self.currency)?;
+        add_currency(w, &self.balance, &self.denominator)?;
+        add_currency(w, &self.received, &self.denominator)?;
+        w.end_group()?;
+
+        w.start_group();
+        w.opt_debug::<Yellow, _>(&self.first_seen)?;
+        w.opt_debug::<Yellow, _>(&self.last_withdrawal)?;
+        w.opt_debug::<Yellow, _>(&self.description)?;
+        w.end_group()?;
+
+        Ok(())
+    }
+
+    #[inline]
+    fn children(&self, _w: &mut fmt::DetailFormatter) -> fmt::Result {
+        Ok(())
+    }
+}
+
+display_detailed!(DetailedCryptoAddr);
+
+impl Detailed for CryptoAddr {
+    type T = DetailedCryptoAddr;
+
+    fn detailed(&self, _db: &Database) -> Result<Self::T> {
+        Ok(DetailedCryptoAddr {
+            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(),
+            unscoped: self.unscoped,
+            description: self.description.clone(),
+        })
+    }
+}
+
+#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
+#[table_name="cryptoaddrs"]
+pub struct NewCryptoAddr {
+    pub value: String,
+    pub currency: Option<String>,
+    pub denominator: Option<i32>,
+    pub balance: Option<i64>,
+    pub received: Option<i64>,
+    pub first_seen: Option<NaiveDateTime>,
+    pub last_withdrawal: Option<NaiveDateTime>,
+    pub unscoped: bool,
+    pub description: Option<String>,
+}
+
+impl InsertableStruct<CryptoAddr> for NewCryptoAddr {
+    fn value(&self) -> &str {
+        &self.value
+    }
+
+    fn set_scoped(&mut self, scoped: bool) {
+        self.unscoped = !scoped;
+    }
+
+    fn insert(&self, db: &Database) -> Result<()> {
+        diesel::insert_into(cryptoaddrs::table)
+            .values(self)
+            .execute(db.db())?;
+        Ok(())
+    }
+}
+
+impl Upsertable<CryptoAddr> for NewCryptoAddr {
+    type Update = CryptoAddrUpdate;
+
+    fn upsert(self, existing: &CryptoAddr) -> Self::Update {
+        Self::Update {
+            id: existing.id,
+            currency: Self::upsert_opt(self.currency, &existing.currency),
+            denominator: Self::upsert_opt(self.denominator, &existing.denominator),
+            balance: Self::upsert_opt(self.balance, &existing.balance),
+            received: Self::upsert_opt(self.received, &existing.received),
+            first_seen: Self::upsert_opt(self.first_seen, &existing.first_seen),
+            last_withdrawal: Self::upsert_opt(self.last_withdrawal, &existing.last_withdrawal),
+            description: Self::upsert_opt(self.description, &existing.description),
+        }
+    }
+}
+
+impl Printable<PrintableCryptoAddr> for NewCryptoAddr {
+    fn printable(&self, _db: &Database) -> Result<PrintableCryptoAddr> {
+        Ok(PrintableCryptoAddr {
+            value: self.value.to_string(),
+        })
+    }
+}
+
+pub type InsertCryptoAddr = NewCryptoAddr;
+
+impl InsertToNew for InsertCryptoAddr {
+    type Target = NewCryptoAddr;
+
+    #[inline]
+    fn try_into_new(self) -> Result<NewCryptoAddr> {
+        Ok(self)
+    }
+}
+
+#[derive(Identifiable, AsChangeset, Serialize, Deserialize, Debug)]
+#[table_name="cryptoaddrs"]
+pub struct CryptoAddrUpdate {
+    pub id: i32,
+    pub currency: Option<String>,
+    pub denominator: Option<i32>,
+    pub balance: Option<i64>,
+    pub received: Option<i64>,
+    pub first_seen: Option<NaiveDateTime>,
+    pub last_withdrawal: Option<NaiveDateTime>,
+    pub description: Option<String>,
+}
+
+impl Upsert for CryptoAddrUpdate {
+    fn is_dirty(&self) -> bool {
+        self.currency.is_some() ||
+        self.denominator.is_some() ||
+        self.balance.is_some() ||
+        self.received.is_some() ||
+        self.first_seen.is_some() ||
+        self.last_withdrawal.is_some() ||
+        self.description.is_some()
+    }
+
+    fn generic(self) -> Update {
+        Update::CryptoAddr(self)
+    }
+
+    fn apply(&self, db: &Database) -> Result<i32> {
+        db.update_cryptoaddr(&self)
+    }
+}
+
+impl Updateable<CryptoAddr> for CryptoAddrUpdate {
+    fn changeset(&mut self, existing: &CryptoAddr) {
+        Self::clear_if_equal(&mut self.currency, &existing.currency);
+        Self::clear_if_equal(&mut self.denominator, &existing.denominator);
+        Self::clear_if_equal(&mut self.balance, &existing.balance);
+        Self::clear_if_equal(&mut self.received, &existing.received);
+        Self::clear_if_greater_or_equal(&mut self.first_seen, &existing.first_seen);
+        Self::clear_if_lower_or_equal(&mut self.last_withdrawal, &existing.last_withdrawal);
+        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);
+    }
+}
+
+// ensure precision by working around floats
+fn display_currency(value: u64, denominator: usize) -> String {
+    let x = format!("{:>0width$}", value, width=denominator+1);
+    let (a, b) = x.split_at(x.len()-denominator);
+    let mut x = format!("{}.{}", a, b).chars().collect::<Vec<_>>();
+
+    loop {
+        match x.last() {
+            Some('0') => x.pop(),
+            Some('.') => {
+                x.pop();
+                break;
+            },
+            _ => break,
+        };
+    }
+
+    x.into_iter().collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_bitcoin_1() {
+        let x = display_currency(0, 8);
+        assert_eq!(x, "0");
+    }
+
+    #[test]
+    fn test_bitcoin_2() {
+        let x = display_currency(1, 8);
+        assert_eq!(x, "0.00000001");
+    }
+
+    #[test]
+    fn test_bitcoin_3() {
+        let x = display_currency(100000000, 8);
+        assert_eq!(x, "1");
+    }
+
+    #[test]
+    fn test_bitcoin_4() {
+        let x = display_currency(10000000000, 8);
+        assert_eq!(x, "100");
+    }
+
+    #[test]
+    fn test_bitcoin_5() {
+        let x = display_currency(123450000, 8);
+        assert_eq!(x, "1.2345");
+    }
+
+    #[test]
+    fn test_bitcoin_6() {
+        let x = display_currency(12345, 8);
+        assert_eq!(x, "0.00012345");
+    }
+
+    #[test]
+    fn test_zero_1() {
+        let x = display_currency(0, 8);
+        assert_eq!(x, "0");
+    }
+
+    #[test]
+    fn test_zero_2() {
+        let x = display_currency(12345, 0);
+        assert_eq!(x, "12345");
+    }
+}
diff --git a/src/models/mod.rs b/src/models/mod.rs
index b7cc775b..214c8a0b 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -24,6 +24,7 @@ pub enum Insert {
     Image(NewImage),
     Port(NewPort),
     Netblock(NewNetblock),
+    CryptoAddr(NewCryptoAddr),
 }
 
 impl Insert {
@@ -57,6 +58,7 @@ impl Insert {
             Insert::Image(x) => format!("{:?}", x.value),
             Insert::Port(x) => format!("{:?}", x.value),
             Insert::Netblock(x) => format!("{:?}", x.value),
+            Insert::CryptoAddr(x) => format!("{:?}", x.value),
         };
         Ok(label)
     }
@@ -84,6 +86,7 @@ impl Insert {
             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)?),
         })
     }
 }
@@ -107,6 +110,7 @@ impl From<&Insert> for Table {
             Insert::Image(_) => Table::Images,
             Insert::Port(_) => Table::Ports,
             Insert::Netblock(_) => Table::Netblocks,
+            Insert::CryptoAddr(_) => Table::Cryptoaddrs,
         }
     }
 }
@@ -126,6 +130,7 @@ pub enum Update {
     Image(ImageUpdate),
     Port(PortUpdate),
     Netblock(NetblockUpdate),
+    CryptoAddr(CryptoAddrUpdate),
 }
 
 impl Update {
@@ -144,6 +149,7 @@ impl Update {
             Update::Image(update)         => update.is_dirty(),
             Update::Port(update)          => update.is_dirty(),
             Update::Netblock(update)      => update.is_dirty(),
+            Update::CryptoAddr(update)    => update.is_dirty(),
         }
     }
 }
@@ -164,6 +170,7 @@ impl fmt::Display for Update {
             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()),
         }
     }
 }
@@ -286,6 +293,14 @@ pub trait Updateable<M> {
         }
     }
 
+    fn clear_if_greater_or_equal<T: PartialOrd>(update: &mut Option<T>, existing: &Option<T>) {
+        if let (Some(new), Some(old)) = (&update, &existing) {
+            if *new >= *old {
+                update.take();
+            }
+        }
+    }
+
     fn changeset(&mut self, existing: &M);
 
     #[inline]
@@ -406,3 +421,6 @@ pub use self::image::*;
 
 mod port;
 pub use self::port::*;
+
+mod cryptoaddr;
+pub use self::cryptoaddr::*;
diff --git a/src/runtime/db.rs b/src/runtime/db.rs
index 0a9f68cf..2577e81c 100644
--- a/src/runtime/db.rs
+++ b/src/runtime/db.rs
@@ -69,6 +69,9 @@ fn into_insert(family: Family, object: LuaJsonValue, state: &Arc<dyn State>) ->
         Family::Netblock => {
             Insert::Netblock(try_into_new::<InsertNetblock>(object, state)?)
         },
+        Family::Cryptoaddr => {
+            Insert::CryptoAddr(try_into_new::<InsertCryptoAddr>(object, state)?)
+        },
     };
     Ok(obj)
 }
@@ -168,6 +171,8 @@ pub fn db_update(lua: &mut hlua::Lua, state: Arc<dyn State>) {
                 .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
diff --git a/src/runtime/int.rs b/src/runtime/int.rs
new file mode 100644
index 00000000..062e37eb
--- /dev/null
+++ b/src/runtime/int.rs
@@ -0,0 +1,78 @@
+use crate::errors::*;
+use crate::engine::ctx::State;
+use crate::hlua::{self, AnyLuaValue};
+use std::sync::Arc;
+
+pub fn intval(lua: &mut hlua::Lua, state: Arc<dyn State>) {
+    lua.set("intval", hlua::function1(move |val: AnyLuaValue| -> Result<AnyLuaValue> {
+        match val {
+            AnyLuaValue::LuaNil => Ok(AnyLuaValue::LuaNil),
+            AnyLuaValue::LuaString(ref x) => {
+                let n = x.parse::<i64>()
+                    .map_err(|e| state.set_error(Error::from(e)))?;
+                Ok(AnyLuaValue::LuaNumber(n as f64))
+            },
+            AnyLuaValue::LuaNumber(ref x) => {
+                Ok(AnyLuaValue::LuaNumber(x.floor()))
+            },
+            _ => Err(state.set_error(format_err!("Unsupported input for intval"))),
+        }
+    }))
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::engine::ctx::Script;
+
+    #[test]
+    fn verify_intval_int() {
+        let script = Script::load_unchecked(r#"
+        function run()
+            x = intval(123)
+            if x ~= 123 then
+                return 'wrong ' .. x
+            end
+        end
+        "#).expect("Failed to load script");
+        script.test().expect("Script failed");
+    }
+
+    #[test]
+    fn verify_intval_str() {
+        let script = Script::load_unchecked(r#"
+        function run()
+            x = intval('123')
+            if x ~= 123 then
+                return 'wrong ' .. x
+            end
+        end
+        "#).expect("Failed to load script");
+        script.test().expect("Script failed");
+    }
+
+    #[test]
+    fn verify_intval_float_1() {
+        let script = Script::load_unchecked(r#"
+        function run()
+            x = intval(0.456)
+            if x ~= 0 then
+                return 'wrong ' .. x
+            end
+        end
+        "#).expect("Failed to load script");
+        script.test().expect("Script failed");
+    }
+
+    #[test]
+    fn verify_intval_float_2() {
+        let script = Script::load_unchecked(r#"
+        function run()
+            x = intval(123.456)
+            if x ~= 123 then
+                return 'wrong ' .. x
+            end
+        end
+        "#).expect("Failed to load script");
+        script.test().expect("Script failed");
+    }
+}
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
index efc2fa6e..885ccc34 100644
--- a/src/runtime/mod.rs
+++ b/src/runtime/mod.rs
@@ -17,6 +17,7 @@ import_fns!(hashes);
 import_fns!(hex);
 import_fns!(http);
 import_fns!(html);
+import_fns!(int);
 import_fns!(json);
 import_fns!(keyring);
 import_fns!(logger);
diff --git a/src/schema.rs b/src/schema.rs
index e483d012..454e7281 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -41,6 +41,21 @@ table! {
     }
 }
 
+table! {
+    cryptoaddrs (id) {
+        id -> Integer,
+        value -> Text,
+        currency -> Nullable<Text>,
+        denominator -> Nullable<Integer>,
+        balance -> Nullable<BigInt>,
+        received -> Nullable<BigInt>,
+        first_seen -> Nullable<Timestamp>,
+        last_withdrawal -> Nullable<Timestamp>,
+        unscoped -> Bool,
+        description -> Nullable<Text>,
+    }
+}
+
 table! {
     devices (id) {
         id -> Integer,
@@ -234,6 +249,7 @@ allow_tables_to_appear_in_same_query!(
     autonoscope,
     breach_emails,
     breaches,
+    cryptoaddrs,
     devices,
     domains,
     emails,
diff --git a/src/shell/complete.rs b/src/shell/complete.rs
index 487512fb..e2799624 100644
--- a/src/shell/complete.rs
+++ b/src/shell/complete.rs
@@ -39,6 +39,8 @@ impl CmdCompleter {
                 "breaches",
                 "images",
                 "ports",
+                "netblocks",
+                "cryptoaddrs",
             ], &args[1]))
         }
     }
@@ -106,7 +108,9 @@ impl Completer for CmdCompleter {
                                         "network",
                                         "account",
                                         "breach",
-                                        "image"];
+                                        "image",
+                                        "netblock",
+                                        "cryptoaddr"];
 
                         let results: Vec<String> = options.iter()
                             .filter(|x| x.starts_with(arg))


Test patch: