///|
fn main {
let args = @env.args()
if args.length() < 2 {
print_usage()
return
}
match args[1] {
"keygen" => cmd_keygen(args)
"sign" => cmd_sign(args)
"verify" => cmd_verify(args)
"trust" => cmd_trust(args)
"typosquat" => cmd_typosquat(args)
"audit" => cmd_audit(args)
"manifest" => cmd_manifest(args)
"hash" => cmd_hash(args)
"version" | "-v" | "--version" => print_version()
"help" | "-h" | "--help" => print_usage()
_ => {
println("Unknown command: " + args[1])
print_usage()
}
}
}
///|
fn print_version() -> Unit {
println("MoonGuard v0.1.11 / MoonGuard v0.1.11")
println(
"MoonBit supply chain security toolchain / MoonBit 供应链安全工具链",
)
println("https://github.com/chenzehaoo/MoonGUARD")
}
///|
fn print_usage() -> Unit {
println(
"MoonGuard - MoonBit Supply Chain Security Tool / MoonBit 供应链安全工具",
)
println("")
println(
"Usage: moon_guard [options] / moon_guard <命令> [选项]",
)
println("")
println("Commands / 命令:")
println(
" keygen [seed] Generate Ed25519 keypair (uses demo seed when omitted) / 生成 Ed25519 密钥对(缺省使用内置 demo seed)",
)
println(
" sign Sign a UTF-8 message with a hex seed / 用 hex seed 签名 UTF-8 消息",
)
println(
" verify Verify an Ed25519 signature / 验证 Ed25519 签名",
)
println(
" trust add [full|partial|untrusted] Add a trusted key / 添加可信密钥",
)
println(
" trust list List trusted keys / 列出可信密钥",
)
println(
" trust remove Remove a trusted key / 删除可信密钥",
)
println(
" typosquat Detect typosquatting (strict + homoglyphs) / 检测 typosquat(严格 + 同形混淆)",
)
println(
" manifest gen [f:c...] Generate manifest from path:content pairs / 从 path:content 对生成清单",
)
println(
" manifest verify [f:c] Verify a manifest against inputs / 校验清单",
)
println(
" hash Compute SHA-256 hash / 计算 SHA-256 哈希",
)
println(
" audit [--signature ] [--trust-store ] [--signer ] [--pubkey ] Run audit against external manifest/signature/trust evidence / 基于外部清单、签名和信任库审计",
)
println(
" version Print version and exit / 打印版本并退出",
)
println(
" help Print this message / 打印帮助",
)
}
///|
fn cmd_keygen(args : Array[String]) -> Unit {
let seed = if args.length() > 2 { args[2] } else { demo_seed() }
let kp = @crypto.generate_keypair(seed)
println("Ed25519 Keypair Generated")
println(" seed: " + seed)
println(" public_key: " + kp.public_key)
println("")
println("PEM envelope:")
println(@trust.public_key_to_pem(kp.public_key))
}
///|
fn cmd_sign(args : Array[String]) -> Unit {
if args.length() < 4 {
println("Usage: moon_guard sign ")
return
}
let msg = args[2]
let seed = args[3]
let kp = @crypto.generate_keypair(seed)
let sig = @crypto.sign(msg, kp)
println("Signature:")
println(" message: " + msg)
println(" public_key: " + kp.public_key)
println(" sig.r: " + sig.r)
println(" sig.s: " + sig.s)
println(" combined: " + (sig.r + sig.s))
}
///|
fn cmd_verify(args : Array[String]) -> Unit {
if args.length() < 5 {
println("Usage: moon_guard verify ")
return
}
let msg = args[2]
let sig_hex = args[3]
let pubkey = args[4]
let sig_r = @manifest.take_chars_(sig_hex, 64)
let sig_s = @manifest.slice_chars_(sig_hex, 64, 128)
let sig = @crypto.Signature::{ r: sig_r, s: sig_s }
if @crypto.verify(msg, sig, pubkey) {
println("Verification: PASS")
} else {
println("Verification: FAIL")
}
}
///|
fn cmd_trust(args : Array[String]) -> Unit {
if args.length() < 3 {
println("Usage: moon_guard trust [args]")
return
}
// Load the persisted trust store (cross-process) instead of starting
// from an empty in-memory store each invocation.
let store = load_trust_store()
match args[2] {
"add" => {
if args.length() < 6 {
println("Usage: moon_guard trust add [level]")
return
}
let key_id = args[3]
let pubkey = args[4]
let owner = args[5]
let level = if args.length() > 6 {
parse_trust_level(args[6])
} else {
@trust.TrustLevel::Full
}
if store.add_key(key_id, pubkey, owner, level) {
println(
"Key added: " +
key_id +
" (owner: " +
owner +
", level: " +
trust_level_name(level) +
")",
)
println("PEM:")
println(@trust.public_key_to_pem(pubkey))
if save_trust_store(store) {
println("Trust store saved to: " + trust_store_path())
} else {
println("Warning: failed to persist trust store")
}
} else {
println(
"Failed to add key (invalid format, duplicate, or missing required argument)",
)
}
}
"list" => {
println("Trust Store: " + store.key_count().to_string() + " keys")
let keys = store.list_all()
for i = 0; i < keys.length(); i = i + 1 {
let k = keys[i]
println(
" [" +
trust_level_name(k.trust_level) +
"] " +
k.key_id +
" (owner: " +
k.owner +
", pubkey: " +
@manifest.take_chars_(k.public_key, 12) +
"...)",
)
}
}
"remove" => {
if args.length() < 4 {
println("Usage: moon_guard trust remove ")
return
}
if store.remove_key(args[3]) {
println("Key removed: " + args[3])
if save_trust_store(store) {
println("Trust store saved to: " + trust_store_path())
} else {
println("Warning: failed to persist trust store")
}
} else {
println("Key not found: " + args[3])
}
}
_ => println("Unknown trust subcommand: " + args[2])
}
}
///|
/// Path of the persistent cross-process trust store file.
fn trust_store_path() -> String {
"moon_guard_trust.json"
}
///|
/// Load the trust store from disk, or start fresh when the file is missing.
fn load_trust_store() -> @trust.TrustStore {
let stored = @trust.load_store_from_file(trust_store_path())
match stored {
Some(s) => s
None => @trust.TrustStore::new()
}
}
///|
fn save_trust_store(store : @trust.TrustStore) -> Bool {
store.save_to_file(trust_store_path())
}
///|
fn cmd_typosquat(args : Array[String]) -> Unit {
if args.length() < 4 {
println("Usage: moon_guard typosquat [known2...]")
return
}
let name = args[2]
let known : Array[String] = []
for i = 3; i < args.length(); i = i + 1 {
known.push(args[i])
}
let results = @verify.detect_typosquat_strict(name, known)
if results.length() == 0 {
println("No typosquat risk detected for: " + name)
} else {
println("Typosquat WARNING for: " + name)
for i = 0; i < results.length(); i = i + 1 {
let r = results[i]
println(
" similar to: " +
r.similar_to +
" (" +
r.attack_type +
", dist=" +
r.distance.to_string() +
")",
)
}
}
}
///|
fn cmd_audit(args : Array[String]) -> Unit {
let pkg_name = if args.length() > 2 { args[2] } else { "demo-package" }
let pkg_ver = if args.length() > 3 { args[3] } else { "1.0.0" }
let report = @report.SecurityReport::new(pkg_name, pkg_ver)
let dir = audit_dir_arg(args)
let entries = if dir.length() > 0 {
let files = @fs.walk(dir) catch { _ => [] }
scan_files_to_entries(dir, files)
} else {
parse_file_entries(args, 4)
}
let manifest = read_audit_manifest(args, dir)
match manifest {
None => {
report.add_finding(@report.Finding::{
title: "Manifest missing or unreadable",
description: "Audit requires an existing manifest.json or --manifest file",
risk_level: @report.RiskLevel::Critical,
remediation: "Generate and ship a manifest before auditing the package",
})
report.recompute_score()
print_audit_report(report, None)
return
}
Some(m) => {
report.file_count = m.total_files
let mismatches = @manifest.verify_manifest(m, entries)
if mismatches.length() > 0 {
report.add_finding(@report.Finding::{
title: "File integrity check failed",
description: mismatches.length().to_string() + " file(s) modified",
risk_level: @report.RiskLevel::Critical,
remediation: "Re-download package from trusted source",
})
}
let sig_hex = read_audit_signature(args, dir)
match sig_hex {
None =>
report.add_finding(@report.Finding::{
title: "External signature missing or invalid",
description: "Audit requires a 128-character signature hex blob from --signature or a .sig file",
risk_level: @report.RiskLevel::Critical,
remediation: "Provide the package author's detached signature",
})
Some(sig) => {
let store = load_audit_trust_store(args, dir)
match store {
None =>
report.add_finding(@report.Finding::{
title: "Trust store missing or unreadable",
description: "Audit requires a valid trust store from --trust-store, package directory, or working directory",
risk_level: @report.RiskLevel::High,
remediation: "Provide a trust store containing the signer's public key",
})
Some(ts) => {
let signer = audit_signer_key(args, ts)
match signer {
None =>
report.add_finding(@report.Finding::{
title: "Signer key id is ambiguous",
description: "Use --signer when the trust store is empty or contains multiple keys",
risk_level: @report.RiskLevel::High,
remediation: "Pass --signer for the package author",
})
Some(key_id) => {
check_external_public_key(args, dir, ts, key_id, report)
let vr = @verify.audit_package(
pkg_name, pkg_ver, m, entries, sig, key_id, ts,
)
report.signature_verified = vr.signature_ok
if !vr.signature_ok {
report.add_finding(@report.Finding::{
title: "Signature verification failed",
description: "Package manifest signature could not be verified with the trusted public key",
risk_level: @report.RiskLevel::Critical,
remediation: "Obtain a valid signature from the package author",
})
}
if vr.trust_status == "untrusted" {
report.add_finding(@report.Finding::{
title: "Signer is explicitly untrusted",
description: "Signer key " +
key_id +
" is marked Untrusted",
risk_level: @report.RiskLevel::Critical,
remediation: "Use a trusted signer key",
})
} else if vr.trust_status == "unknown" {
report.add_finding(@report.Finding::{
title: "Signer is not trusted",
description: "Signer key " +
key_id +
" is not present in the trust store",
risk_level: @report.RiskLevel::High,
remediation: "Add the signer key to the trust store after out-of-band verification",
})
}
}
}
}
}
}
}
// Typosquat check (strict mode).
let known = [
"lodash", "express", "react", "angular", "moment", "axios", "moonguard",
]
let typo_results = @verify.detect_typosquat_strict(pkg_name, known)
for i = 0; i < typo_results.length(); i = i + 1 {
report.typosquat_suspects.push(typo_results[i].similar_to)
report.add_finding(@report.Finding::{
title: "Typosquat risk: similar to " + typo_results[i].similar_to,
description: "Attack type: " + typo_results[i].attack_type,
risk_level: @report.RiskLevel::High,
remediation: "Verify package name is correct",
})
}
report.recompute_score()
print_audit_report(report, Some(m))
}
}
}
///|
fn print_audit_report(
report : @report.SecurityReport,
manifest : @manifest.Manifest?,
) -> Unit {
println(report.summary())
println("--- Manifest ---")
match manifest {
Some(m) => {
println("(audited against external manifest)")
println(@manifest.manifest_summary(m))
}
None => println("(no external manifest available)")
}
println("")
println("--- JSON Report ---")
println(@report.report_to_json(report))
}
///|
/// Build `FileEntry` list by reading the actual file contents under `dir`.
/// Paths recorded in the manifest are relative to `dir`.
fn scan_files_to_entries(
dir : String,
files : Array[String],
) -> Array[@manifest.FileEntry] {
let entries : Array[@manifest.FileEntry] = []
for i = 0; i < files.length(); i = i + 1 {
let rel = files[i]
// Skip the manifest / signature themselves so they are not hashed into
// the manifest they verify.
if rel == "manifest.json" ||
rel == "moon_guard_trust.json" ||
rel.has_suffix(".sig") ||
rel.has_suffix(".pem") ||
rel.has_suffix(".pub") {
continue
}
let full = dir + "/" + rel
let content = @fs.read_file_to_string(full) catch { _ => continue }
entries.push(@manifest.FileEntry::{ path: rel, content })
}
entries
}
///|
/// Load `manifest.json` from `dir` (when present) as the manifest to verify
/// against. Falls back to `None` so callers use a freshly generated one.
fn read_existing_manifest(
dir : String,
_pkg : String,
_ver : String,
) -> @manifest.Manifest? {
if dir.length() == 0 {
return None
}
let path = dir + "/manifest.json"
if !@fs.path_exists(path) {
return None
}
let text = @fs.read_file_to_string(path) catch { _ => return None }
@manifest.manifest_from_json(text)
}
///|
fn read_manifest_file(path : String) -> @manifest.Manifest? {
if !@fs.path_exists(path) {
return None
}
let text = @fs.read_file_to_string(path) catch { _ => return None }
@manifest.manifest_from_json(text)
}
///|
fn read_audit_manifest(
args : Array[String],
dir : String,
) -> @manifest.Manifest? {
match arg_value(args, "--manifest") {
Some(path) => read_manifest_file(path)
None => read_existing_manifest(dir, "", "")
}
}
///|
/// Look for a `.sig` file directly inside `dir`, if any. The returned path
/// includes `dir`, so callers can read the file from outside the cwd.
fn find_sig_file(dir : String) -> String? {
if dir.length() == 0 {
return None
}
let files = @fs.read_dir(dir) catch { _ => return None }
for i = 0; i < files.length(); i = i + 1 {
if files[i].has_suffix(".sig") {
return Some(join_path(dir, files[i]))
}
}
None
}
///|
fn read_audit_signature(args : Array[String], dir : String) -> String? {
match arg_value(args, "--signature") {
Some(value) => read_signature_input(value)
None =>
match find_sig_file(dir) {
Some(path) => read_signature_input(path)
None => None
}
}
}
///|
fn read_signature_input(input : String) -> String? {
let text = if @fs.path_exists(input) {
@fs.read_file_to_string(input) catch {
_ => return None
}
} else {
input
}
normalize_hex(text, 128)
}
///|
fn load_audit_trust_store(
args : Array[String],
dir : String,
) -> @trust.TrustStore? {
match arg_value(args, "--trust-store") {
Some(path) => @trust.load_store_from_file(path)
None => {
if dir.length() > 0 {
let store_path = join_path(dir, trust_store_path())
if @fs.path_exists(store_path) {
return @trust.load_store_from_file(store_path)
}
}
@trust.load_store_from_file(trust_store_path())
}
}
}
///|
fn audit_signer_key(args : Array[String], store : @trust.TrustStore) -> String? {
match arg_value(args, "--signer") {
Some(s) => Some(s)
None => {
let keys = store.list_all()
if keys.length() == 1 {
Some(keys[0].key_id)
} else {
None
}
}
}
}
///|
fn check_external_public_key(
args : Array[String],
dir : String,
store : @trust.TrustStore,
signer : String,
report : @report.SecurityReport,
) -> Unit {
let external_key = read_audit_public_key(args, dir)
match external_key {
None => ()
Some(pubkey) =>
match store.get_key(signer) {
Some(k) =>
if k.public_key != pubkey {
report.add_finding(@report.Finding::{
title: "Public key does not match trust store",
description: "The external public key differs from trusted signer " +
signer,
risk_level: @report.RiskLevel::Critical,
remediation: "Verify the signer's public key out of band and update the trust store",
})
}
None => ()
}
}
}
///|
fn read_audit_public_key(args : Array[String], dir : String) -> String? {
match arg_value(args, "--pubkey") {
Some(value) => read_public_key_input(value)
None => {
if dir.length() == 0 {
return None
}
let candidates = [
"public_key.pem", "moon_guard_public.pem", "moonguard_public.pem", "public_key.pub",
"moon_guard.pub",
]
for i = 0; i < candidates.length(); i = i + 1 {
let path = join_path(dir, candidates[i])
if @fs.path_exists(path) {
return read_public_key_input(path)
}
}
None
}
}
}
///|
fn read_public_key_input(input : String) -> String? {
let text = if @fs.path_exists(input) {
@fs.read_file_to_string(input) catch {
_ => return None
}
} else {
input
}
match @trust.public_key_from_pem(text) {
Some(pubkey) => Some(pubkey)
None => normalize_hex(text, 64)
}
}
///|
fn normalize_hex(text : String, expected_len : Int) -> String? {
let buf = StringBuilder::new(size_hint=expected_len)
for i = 0; i < text.length(); i = i + 1 {
let c = text[i]
if c == ' ' || c == '\n' || c == '\r' || c == '\t' {
continue
}
if c >= 48 && c <= 57 {
buf.write_char(c.unsafe_to_char())
} else if c >= 97 && c <= 102 {
buf.write_char(c.unsafe_to_char())
} else if c >= 65 && c <= 70 {
buf.write_char((c + 32).unsafe_to_char())
} else {
return None
}
}
let value = buf.to_string()
if value.length() == expected_len {
Some(value)
} else {
None
}
}
///|
fn arg_value(args : Array[String], name : String) -> String? {
for i = 0; i < args.length() - 1; i = i + 1 {
if args[i] == name {
return Some(args[i + 1])
}
}
None
}
///|
fn audit_dir_arg(args : Array[String]) -> String {
if args.length() <= 4 {
return ""
}
let candidate = args[4]
if candidate.has_prefix("--") {
return ""
}
if @fs.path_exists(candidate) {
let is_dir = @fs.is_dir(candidate) catch { _ => false }
if is_dir {
return candidate
}
}
""
}
///|
fn join_path(dir : String, name : String) -> String {
if dir.has_suffix("/") || dir.has_suffix("\\") {
dir + name
} else {
dir + "/" + name
}
}
///|
fn cmd_hash(args : Array[String]) -> Unit {
if args.length() < 3 {
println("Usage: moon_guard hash ")
return
}
let content = args[2]
let hash = @manifest.sha256(content)
println("SHA-256: " + hash)
}
///|
fn cmd_manifest(args : Array[String]) -> Unit {
if args.length() < 3 {
println(
"Usage: moon_guard manifest [file:content...]",
)
return
}
match args[2] {
"gen" => cmd_manifest_gen(args)
"verify" => cmd_manifest_verify(args)
_ => println("Unknown manifest subcommand: " + args[2])
}
}
///|
fn cmd_manifest_gen(args : Array[String]) -> Unit {
if args.length() < 5 {
println("Usage: moon_guard manifest gen [file:content...]")
return
}
let pkg = args[3]
let ver = args[4]
let entries = parse_file_entries(args, 5)
let m = @manifest.generate_manifest(pkg, ver, entries)
println(@manifest.manifest_summary(m))
println("--- JSON ---")
println(@manifest.manifest_to_json(m))
}
///|
fn cmd_manifest_verify(args : Array[String]) -> Unit {
if args.length() < 5 {
println("Usage: moon_guard manifest verify [file:content...]")
return
}
let pkg = args[3]
let ver = args[4]
let entries = parse_file_entries(args, 5)
// Verify the given file contents against the manifest that shipped on disk
// (manifest.json in the current directory), not against a manifest generated
// from the same inputs — otherwise the check is trivially self-consistent
// and never fails.
let existing = read_existing_manifest(".", pkg, ver)
match existing {
Some(m) => {
let mismatches = @manifest.verify_manifest(m, entries)
if mismatches.length() == 0 {
println("Manifest verification: PASS (all files intact)")
} else {
println("Manifest verification: FAIL")
for i = 0; i < mismatches.length(); i = i + 1 {
println(" mismatch: " + mismatches[i])
}
}
}
None =>
println(
"No manifest.json found in the current directory to verify against",
)
}
}
///|
fn parse_file_entries(
args : Array[String],
start : Int,
) -> Array[@manifest.FileEntry] {
let entries : Array[@manifest.FileEntry] = []
for i = start; i < args.length(); i = i + 1 {
let arg = args[i]
let colon_idx = find_char(arg, ':')
if colon_idx >= 0 {
let path = @manifest.take_chars_(arg, colon_idx)
let content = @manifest.slice_chars_(arg, colon_idx + 1, arg.length())
entries.push(@manifest.FileEntry::{ path, content })
}
}
if entries.length() == 0 {
entries.push(@manifest.FileEntry::{
path: "demo.mbt",
content: "fn main {}",
})
entries.push(@manifest.FileEntry::{
path: "lib.mbt",
content: "pub fn hello() -> String { \"hi\" }",
})
}
entries
}
///|
fn find_char(s : String, c : Int) -> Int {
for i = 0; i < s.length(); i = i + 1 {
if s[i].to_int() == c {
return i
}
}
-1
}
///|
fn demo_seed() -> String {
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
}
///|
fn parse_trust_level(s : String) -> @trust.TrustLevel {
if s == "partial" {
@trust.TrustLevel::Partial
} else if s == "untrusted" {
@trust.TrustLevel::Untrusted
} else {
@trust.TrustLevel::Full
}
}
///|
fn trust_level_name(level : @trust.TrustLevel) -> String {
match level {
@trust.TrustLevel::Full => "full"
@trust.TrustLevel::Partial => "partial"
@trust.TrustLevel::Untrusted => "untrusted"
}
}