241 lines
7.2 KiB
Nim
241 lines
7.2 KiB
Nim
# ©AngelaMos | 2026
|
|
# test_all.nim
|
|
#
|
|
# Unit tests for core utility and parsing functions
|
|
#
|
|
# Exercises exported helpers from four modules across eight test
|
|
# suites. redactValue covers short, long, exact-length, and empty
|
|
# strings. isPrivateKey validates detection of five PEM header
|
|
# formats (OpenSSH, RSA, ECDSA, DSA, PKCS8) and rejection of
|
|
# public keys and non-key content. isEncrypted checks for
|
|
# ENCRYPTED, bcrypt, and aes256-ctr markers versus unencrypted
|
|
# keys. matchesSecretPattern verifies detection of export-prefixed
|
|
# and bare KEY=/SECRET=/TOKEN=/PASSWORD= assignments while
|
|
# rejecting PATH exports and ordinary commands.
|
|
# matchesCommandPattern tests curl with auth headers and -u flag,
|
|
# wget with authorization header and password, mysql -p, psql
|
|
# password, and sshpass detection, rejecting safe commands.
|
|
# matchesExclude validates exact filename and directory segment
|
|
# matching without false positives on partial or embedded
|
|
# substrings. permissionSeverity confirms svInfo for nonexistent
|
|
# paths. parseModules tests single, multiple, whitespace-padded,
|
|
# full-set, empty, and unknown module string parsing. redactLine
|
|
# checks export-prefixed quoted, unquoted, and single-quoted value
|
|
# redaction plus passthrough for lines without an equals sign.
|
|
#
|
|
# Connects to:
|
|
# types.nim - Category enum values for parseModules
|
|
# collectors/base.nim - redactValue, matchesExclude,
|
|
# permissionSeverity
|
|
# collectors/ssh.nim - isPrivateKey, isEncrypted
|
|
# collectors/history.nim - matchesSecretPattern,
|
|
# matchesCommandPattern, redactLine
|
|
# harvester.nim - parseModules
|
|
|
|
import std/[unittest, strutils]
|
|
import types
|
|
import collectors/base
|
|
import collectors/ssh
|
|
import collectors/history
|
|
import harvester
|
|
|
|
suite "redactValue":
|
|
test "short value fully redacted":
|
|
check redactValue("abc", 4) == "***"
|
|
|
|
test "value longer than showChars":
|
|
check redactValue("mysecret", 4) == "myse****"
|
|
|
|
test "exact showChars length":
|
|
check redactValue("abcd", 4) == "****"
|
|
|
|
test "empty string":
|
|
check redactValue("", 4) == ""
|
|
|
|
suite "isPrivateKey":
|
|
test "OpenSSH key":
|
|
check isPrivateKey("-----BEGIN OPENSSH PRIVATE KEY-----\ndata")
|
|
|
|
test "RSA key":
|
|
check isPrivateKey("-----BEGIN RSA PRIVATE KEY-----\ndata")
|
|
|
|
test "ECDSA key":
|
|
check isPrivateKey("-----BEGIN EC PRIVATE KEY-----\ndata")
|
|
|
|
test "DSA key":
|
|
check isPrivateKey("-----BEGIN DSA PRIVATE KEY-----\ndata")
|
|
|
|
test "generic PKCS8 key":
|
|
check isPrivateKey("-----BEGIN PRIVATE KEY-----\ndata")
|
|
|
|
test "public key rejected":
|
|
check isPrivateKey("-----BEGIN PUBLIC KEY-----\ndata") == false
|
|
|
|
test "random text rejected":
|
|
check isPrivateKey("this is not a key") == false
|
|
|
|
test "empty string rejected":
|
|
check isPrivateKey("") == false
|
|
|
|
suite "isEncrypted":
|
|
test "ENCRYPTED marker":
|
|
check isEncrypted(
|
|
"-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\ndata"
|
|
)
|
|
|
|
test "bcrypt marker":
|
|
check isEncrypted(
|
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nbcrypt\ndata"
|
|
)
|
|
|
|
test "aes256-ctr marker":
|
|
check isEncrypted("data with aes256-ctr in it")
|
|
|
|
test "unencrypted key":
|
|
check isEncrypted(
|
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nAAAAB3NzaC1\ndata"
|
|
) == false
|
|
|
|
suite "matchesSecretPattern":
|
|
test "export with KEY=":
|
|
check matchesSecretPattern("export API_KEY=some_value")
|
|
|
|
test "export with SECRET=":
|
|
check matchesSecretPattern(
|
|
"export AWS_SECRET_ACCESS_KEY=abc123"
|
|
)
|
|
|
|
test "bare TOKEN= at start":
|
|
check matchesSecretPattern("TOKEN=abcdef12345")
|
|
|
|
test "bare PASSWORD=":
|
|
check matchesSecretPattern("PASSWORD=hunter2")
|
|
|
|
test "non-secret assignment":
|
|
check matchesSecretPattern("export PATH=/usr/bin") == false
|
|
|
|
test "ordinary command":
|
|
check matchesSecretPattern("ls -la /tmp") == false
|
|
|
|
test "empty string":
|
|
check matchesSecretPattern("") == false
|
|
|
|
suite "matchesCommandPattern":
|
|
test "curl with auth header":
|
|
check matchesCommandPattern(
|
|
"curl -H \"Authorization: Bearer token\" https://api.example.com"
|
|
)
|
|
|
|
test "curl with lowercase -h auth":
|
|
check matchesCommandPattern(
|
|
"curl -h \"authorization: bearer token\" https://api.example.com"
|
|
)
|
|
|
|
test "curl with -u flag":
|
|
check matchesCommandPattern(
|
|
"curl -u user:pass https://api.example.com"
|
|
)
|
|
|
|
test "wget with authorization header":
|
|
check matchesCommandPattern(
|
|
"wget --header=\"Authorization: Basic abc\" https://example.com"
|
|
)
|
|
|
|
test "wget with password":
|
|
check matchesCommandPattern(
|
|
"wget --password=secret https://files.example.com/data.zip"
|
|
)
|
|
|
|
test "mysql with -p flag":
|
|
check matchesCommandPattern("mysql -u root -psecret mydb")
|
|
|
|
test "psql with password":
|
|
check matchesCommandPattern(
|
|
"psql password=secret host=db.example.com"
|
|
)
|
|
|
|
test "sshpass command":
|
|
check matchesCommandPattern("sshpass -p 'mypass' ssh user@host")
|
|
|
|
test "safe curl rejected":
|
|
check matchesCommandPattern("curl https://example.com") == false
|
|
|
|
test "safe git command rejected":
|
|
check matchesCommandPattern("git push origin main") == false
|
|
|
|
test "empty string rejected":
|
|
check matchesCommandPattern("") == false
|
|
|
|
suite "matchesExclude":
|
|
test "exact filename match":
|
|
check matchesExclude("/home/user/.env", @[".env"])
|
|
|
|
test "directory segment match":
|
|
check matchesExclude("/home/user/.git/config", @[".git"])
|
|
|
|
test "no false positive on partial name":
|
|
check matchesExclude(
|
|
"/home/user/.venv/lib/site.py", @[".env"]
|
|
) == false
|
|
|
|
test "no match on embedded substring":
|
|
check matchesExclude(
|
|
"/home/user/environment/data", @[".env"]
|
|
) == false
|
|
|
|
test "empty patterns":
|
|
check matchesExclude("/home/user/.env", @[]) == false
|
|
|
|
suite "permissionSeverity":
|
|
test "returns svInfo for unreadable path":
|
|
check permissionSeverity("/nonexistent/path/abc123") == svInfo
|
|
|
|
suite "parseModules":
|
|
test "single module":
|
|
check parseModules("ssh") == @[catSsh]
|
|
|
|
test "multiple modules":
|
|
let mods = parseModules("ssh,git,cloud")
|
|
check mods.len == 3
|
|
check mods.contains(catSsh)
|
|
check mods.contains(catGit)
|
|
check mods.contains(catCloud)
|
|
|
|
test "with whitespace":
|
|
let mods = parseModules(" browser , keyring ")
|
|
check mods.len == 2
|
|
check mods.contains(catBrowser)
|
|
check mods.contains(catKeyring)
|
|
|
|
test "all modules":
|
|
let mods = parseModules(
|
|
"browser,ssh,cloud,history,keyring,git,apptoken"
|
|
)
|
|
check mods.len == 7
|
|
|
|
test "empty string":
|
|
check parseModules("").len == 0
|
|
|
|
test "unknown module ignored":
|
|
check parseModules("fake,nonexistent").len == 0
|
|
|
|
suite "redactLine":
|
|
test "export with quoted value":
|
|
let got = redactLine("export KEY=\"secret\"")
|
|
check got.contains("KEY=")
|
|
check got.contains("\"") == false
|
|
|
|
test "export with unquoted value":
|
|
let got = redactLine("export API_KEY=mysecretvalue")
|
|
check got.contains("API_KEY=")
|
|
check got.contains("myse")
|
|
check got.contains("cretvalue") == false
|
|
|
|
test "no equals sign":
|
|
check redactLine("no assignment here") == "no assignment here"
|
|
|
|
test "single-quoted value":
|
|
let got = redactLine("export TOKEN='abcdefgh'")
|
|
check got.contains("TOKEN=")
|
|
check got.contains("'") == false
|