moonvigil

    Offline dependency vulnerability scanner for MoonBit projects.

    moonbit
    security
    scanner
    dependencies
    supply-chain
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    9 days ago
    Downloads
    15

    #MoonVigil

    MoonVigil is an offline-first dependency vulnerability scanner for MoonBit projects. It reads current moon.mod and moon.pkg manifests and retains support for legacy moon.mod.json / moon.pkg.json files. If both formats are present in one directory, the current format takes precedence. It matches declared module versions against a local versioned advisory database and emits terminal, JSON, SARIF, or CycloneDX-style SBOM output. No project metadata is sent over the network.

    Recursive scans normalize declarations by module name and version. Duplicate declarations are reported once with a deterministic relative manifest location as evidence. Package imports retain runtime, test, and white-box test scopes; aliases do not change the canonical module name.

    #Install

    Add the library to a MoonBit module:

    moon add shen-E/moonvigil@0.2.0

    Import shen-E/moonvigil in the consuming package's moon.pkg. The native command can be installed independently:

    moon install shen-E/moonvigil/cmd/moonvigil@0.2.0 moonvigil --version moonvigil scan ./your-project --db ./advisories.json

    The advisory database is an explicit local input. Download or maintain it separately; installing the command does not update vulnerability data.

    #Run from source

    The checked native toolchain is moon 0.1.20261023 / moonc v0.10.17. For the full build, regression, and CLI acceptance checks on Windows, Linux, or macOS, also install Python 3.8 or newer:

    python scripts/acceptance.py

    This writes actual command results and sample reports to .acceptance-output/summary.json and the same output directory. See docs/acceptance.md for the September hackathon criteria, validation coverage, and current evidence. CI runs this same script on all three operating systems and uploads the results.

    moon check --target native moon test --target native moon run cmd/moonvigil --target native -- db validate advisories.json moon run cmd/moonvigil --target native -- baseline validate fixtures/baseline/valid-v2.json moon run cmd/moonvigil --target native -- scan fixtures/affected --db advisories.json moon run cmd/moonvigil --target native -- scan fixtures/affected --db advisories.json --format sarif --output report.sarif moon run cmd/moonvigil --target native -- sbom fixtures/affected --output bom.json moon run cmd/moonvigil --target native -- scan fixtures/real/moonbitlang-parser --db fixtures/empty-database.json --format json moon run cmd/moonvigil --target native -- scan fixtures/affected --policy fixtures/policy/exact-suppression.json --format json --output report.json moon run cmd/moonvigil --target native -- scan fixtures/affected --fail-on critical moon run cmd/moonvigil --target native -- scan fixtures/scopes --db fixtures/scopes/advisories.json --fail-on-scope runtime,test moon run cmd/moonvigil --target native -- scan fixtures/affected --baseline fixtures/baseline/empty.json moon run cmd/moonvigil --target native -- scan fixtures/affected --baseline-output baseline.json

    #Linux demonstration

    The reproducible WSL2 Ubuntu demonstration is in demo/. It builds and tests MoonVigil, scans a representative multi-manifest project, and writes terminal, JSON, SARIF, and SBOM evidence without executing the scanned project.

    #Advisory database

    advisories.json uses a compact OSV-inspired schema. Use an empty fixed_version string when no fixed release is known. affected accepts three-part numeric stable SemVer expressions using <, <=, >, >=, =, or a bare exact version; spaces form an intersection and || forms a union. * matches every comparable version. Pre-release, build-metadata, and invalid versions or ranges are reported separately rather than treated as vulnerable.

    Every database is validated before scanning. The supported database schema version is "1"; advisory IDs must be unique; required fields are reported with their advisory index and field name; package names and summaries must be present; and severity must be one of critical, high, medium, low, or info. Each advisory needs at least one HTTP(S) reference with a valid authority. A fixed version is empty or a stable three-part version. The db validate command reports all semantic issues found in one pass.

    The JSON report schema is version 6 and includes dependency scopes, effective policy scope gates and out-of-scope counts, plus optional baseline comparison state. A pinned public manifest fixture from moonbitlang/parser is provided under fixtures/real to exercise real-world manifest syntax; its source commit and license are documented alongside the fixture. This is compatibility evidence, not a claim that the project has a known vulnerability.

    #CI policy

    Policy files are loaded only when explicitly passed with --policy; MoonVigil does not auto-discover a policy. --fail-on can override the configured severity threshold. Without a baseline, existing behavior is unchanged: any affected dependency exits 1, no affected dependencies exit 0, and invalid arguments or inputs exit 2. With a policy, only unsuppressed findings at or above the threshold block; uncomparable versions never block.

    Policy schema v2 adds fail_on_scopes; schema v1 remains supported and gates all scopes by default. Select one or more of runtime, test, wbtest, other, and unclassified; an empty scope list on a dependency is treated as unclassified. A dependency used in multiple scopes is in scope when any one of its scopes is selected. The special value all selects every scope and must be used alone. The optional --fail-on-scope runtime,test CLI setting overrides policy scopes for one scan and can be used without a policy file. Scope filtering changes only the blocking decision: every finding remains in terminal, JSON, and SARIF output. Policy summaries include effective scopes and the count of findings excluded from the gate.

    When --baseline <file> is supplied, the default gate blocks new affected findings and findings whose advisory risk evidence changed. Existing findings and snapshot keys no longer detected remain visible but do not block. If a policy or --fail-on threshold is also supplied, that severity threshold applies to new and updated findings using the current advisory severity; active suppressions continue to suppress exact matching findings. Without a baseline, policy behavior is unchanged.

    Suppression rules require an advisory ID, canonical package name, exact stable version, non-empty reason, and an ISO expiry date. A rule applies only to that exact triple, stays active through its expiry date, and is rejected after expiry. Suppressed findings remain in terminal, JSON, and SARIF reports. Unmatched rules produce a warning but do not suppress anything. See docs/ci-policy.md for the schema and workflow details.

    New baseline snapshots store advisory ID, canonical package name, exact version, and the advisory's affected range, severity, and fixed version—never project paths. Schema-v1 snapshots remain readable as legacy snapshots; they are explicitly identified because they cannot detect risk-evidence changes. When a snapshot is supplied, findings are marked new, existing, or updated; keys in the snapshot that are no longer detected are reported separately without assuming they were remediated. Use --baseline-output <file> to explicitly write a current snapshot. See docs/baseline.md for the schema and comparison behavior. Use baseline validate <file> to check either supported baseline schema before using it in CI.

    At the time of this check, the official OSV schema's defined ecosystem list does not include MoonBit or Mooncakes. The pinned public fixture therefore verifies real manifest parsing and inventory only; advisories from other ecosystems are not matched by package-name coincidence, and no real MoonBit vulnerability hit is claimed.

    #Scope and security boundary

    MoonVigil scans only declared direct dependencies from module manifests. Package imports are used to annotate where a declared dependency is used. It does not query the network, execute project code, infer lock files or transitive dependencies, perform source-code SAST, or claim that an unknown version is safe.

    Usage scopes are attached within the nearest module boundary before inventory deduplication, including legacy package import arrays. A test import in a nested project cannot change another project's usage scopes. Malformed import declarations stop a scan with a source and line diagnostic. An empty directory or an unrelated file is an input error.

    The scanner rejects symbolic links to directories or manifests, including Windows reparse points, to prevent traversal loops and scanning outside the selected tree. Unrelated file links are ignored. Output paths must be distinct from input manifests, databases, policies, baselines, and one another; checks include relative path aliases and existing hard links or symbolic links.

    #Development

    just check just test just build

    The native CLI uses small C bridges for process exit codes, file identity, and link detection. Exit codes are 0 when no finding meets the configured gate, 1 when one or more unsuppressed findings meet it, and 2 for invalid command arguments, paths, databases, baselines, policies, or output files.

    BaselineValidationError

    pub(all) suberror BaselineValidationError {
    UnsupportedBaselineSchemaVersion(String)
    InvalidBaselineField(field~ : String, reason~ : String)
    InvalidBaselineEntry(index~ : Int, field~ : String, reason~ : String)
    DuplicateBaselineEntry(index~ : Int, advisory_id~ : String)
    } derive(Eq,
    Debug
    )

    DatabaseValidationError

    pub(all) suberror DatabaseValidationError {
    UnsupportedSchemaVersion(String)
    InvalidDatabaseField(field~ : String, reason~ : String)
    InvalidAdvisory(index~ : Int, id~ : String, field~ : String, reason~ : String)
    MultipleInvalidAdvisories(Array[DatabaseIssue])
    } derive(Eq,
    Debug
    )

    ManifestParseError

    pub(all) suberror ManifestParseError {
    InvalidManifest(source~ : String, line~ : Int, reason~ : String)
    } derive(Eq,
    Debug
    )

    PolicyValidationError

    pub(all) suberror PolicyValidationError {
    InvalidPolicyField(field~ : String, reason~ : String)
    ExpiredSuppression(index~ : Int, advisory_id~ : String, expires_on~ : String)
    } derive(Eq,
    Debug
    )

    Advisory

    pub struct Advisory {
    id : String
    package_name : String
    affected : String
    severity : String
    summary : String
    fixed_version : String
    references : Array[String]
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    AdvisoryDatabase

    pub struct AdvisoryDatabase {
    schema_version : String
    advisories : Array[Advisory]
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    BaselineEntry

    pub struct BaselineEntry {
    advisory_id : String
    package_name : String
    version : String
    affected : String?
    severity : String?
    fixed_version : String?
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    A portable baseline key. It deliberately contains no project paths.

    BaselineFindingStatus

    pub enum BaselineFindingStatus {
    New
    Existing
    Updated
    } derive(Eq, ToJson,
    Debug
    )

    Whether an affected finding is new or already present in the supplied snapshot.

    BaselineSummary

    pub struct BaselineSummary {
    snapshot_entries : Int
    new_findings : Int
    existing_findings : Int
    updated_findings : Int
    legacy_snapshot : Bool
    no_longer_detected : Array[BaselineEntry]
    } derive(Eq, ToJson,
    Debug
    )

    Summary of a baseline comparison, including keys no longer observed.

    DatabaseIssue

    pub(all) struct DatabaseIssue {
    index : Int
    id : String
    field : String
    reason : String
    } derive(Eq,
    Debug
    )

    A semantic validation failure in a local advisory database.

    Dependency

    pub struct Dependency {
    name : String
    version : String
    source : String
    aliases : Array[String]
    scopes : Array[DependencyScope]
    } derive(Eq, ToJson,
    Debug
    )

    Offline dependency vulnerability matching for MoonBit projects.

    Dependency::Dependency

    fn Dependency::Dependency(name~ : String, version~ : String, source~ : String, aliases? : Array[String], scopes? : Array[DependencyScope]) -> Dependency

    DependencyConflict

    pub struct DependencyConflict {
    name : String
    versions : Array[String]
    sources : Array[String]
    } derive(Eq, ToJson,
    Debug
    )

    A declared module with multiple versions in the scanned project tree.

    DependencyScope

    pub(all) enum DependencyScope {
    Runtime
    Test
    WhiteboxTest
    Other
    } derive(Eq,
    Debug
    )

    The dependency usage context recorded in a MoonBit package manifest.

    Finding

    pub struct Finding {
    dependency : Dependency
    advisory : Advisory
    status : FindingStatus
    suppression : PolicySuppression?
    baseline_status : BaselineFindingStatus?
    } derive(Eq, ToJson,
    Debug
    )

    FindingStatus

    pub enum FindingStatus {
    Affected
    NotAffected
    Uncomparable
    } derive(Eq, ToJson,
    Debug
    )

    Inventory

    pub struct Inventory {
    dependencies : Array[Dependency]
    conflicts : Array[DependencyConflict]
    } derive(Eq, ToJson,
    Debug
    )

    A deterministic, de-duplicated dependency inventory.

    PackageImport

    pub struct PackageImport {
    package_name : String
    alias_name : String
    scope : DependencyScope
    source : String
    } derive(Eq, ToJson,
    Debug
    )

    A package import declared in a modern moon.pkg manifest.

    PackageImport::PackageImport

    fn PackageImport::PackageImport(package_name~ : String, alias_name~ : String, scope~ : DependencyScope, source~ : String) -> PackageImport

    PolicyEvaluation

    pub struct PolicyEvaluation {
    report : ScanReport
    should_fail : Bool
    } derive(Eq,
    Debug
    )

    PolicySummary

    pub struct PolicySummary {
    fail_on : String
    fail_on_scopes : Array[String]
    blocking_findings : Int
    scope_excluded_findings : Int
    suppressed_findings : Int
    unmatched_suppressions : Array[PolicySuppression]
    } derive(Eq, ToJson,
    Debug
    )

    Policy outcome attached to a scan report.

    PolicySuppression

    pub struct PolicySuppression {
    advisory_id : String
    package_name : String
    version : String
    reason : String
    expires_on : String
    } derive(Eq, ToJson,
    Debug
    )

    A narrowly scoped, auditable suppression rule.

    ScanBaseline

    pub struct ScanBaseline {
    schema_version : String
    entries : Array[BaselineEntry]
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    A validated, versioned snapshot of affected direct dependencies.

    ScanPolicy

    pub struct ScanPolicy {
    schema_version : String
    fail_on : String
    fail_on_scopes : Array[String]
    suppressions : Array[PolicySuppression]
    } derive(Eq, ToJson,
    Debug
    )

    CI policy for severity/scope gating and exact-version suppressions.

    ScanReport

    pub struct ScanReport {
    schema_version : String
    dependencies : Array[Dependency]
    findings : Array[Finding]
    uncomparable : Array[Finding]
    conflicts : Array[DependencyConflict]
    policy : PolicySummary?
    baseline : BaselineSummary?
    } derive(Eq, ToJson,
    Debug
    )

    apply_baseline

    fn apply_baseline(report : ScanReport, baseline : ScanBaseline) -> ScanReport raise

    Mark findings against a validated snapshot and report snapshot keys no longer present in the current affected findings.

    apply_package_imports

    fn apply_package_imports(dependencies : Array[Dependency], imports : Array[PackageImport]) -> Array[Dependency]

    Attach package usage scopes to their declared module dependencies.

    Package aliases never affect the canonical module identity. A package import is associated with the longest matching declared module path.

    baseline_from_report

    fn baseline_from_report(report : ScanReport) -> ScanBaseline

    Build a deterministic snapshot from affected findings only. Uncomparable versions are omitted because they cannot be matched safely.

    baseline_json

    fn baseline_json(baseline : ScanBaseline) -> String raise

    Serialize a validated snapshot as stable, indented JSON.

    dependency_conflicts

    fn dependency_conflicts(dependencies : Array[Dependency]) -> Array[DependencyConflict]

    Find modules declared at more than one version and sort their evidence.

    dependency_scope_name

    fn dependency_scope_name(scope : DependencyScope) -> String

    empty_policy

    fn empty_policy(fail_on~ : String) -> ScanPolicy

    Make an all-scopes policy for callers that use command-line gate overrides.

    evaluate_policy

    fn evaluate_policy(report : ScanReport, policy : ScanPolicy, fail_on_override : String?, today~ : String) -> PolicyEvaluation raise

    Apply threshold and exact-match suppressions while retaining every finding.

    evaluate_policy_with_scopes

    fn evaluate_policy_with_scopes(report : ScanReport, policy : ScanPolicy, fail_on_override : String?, fail_on_scopes_override : Array[String]?, today~ : String) -> PolicyEvaluation raise

    Apply severity, scope, baseline, and exact-match suppression rules while retaining all findings in the report. A scope override replaces policy scopes for this evaluation only.

    inventory_from_dependencies

    fn inventory_from_dependencies(dependencies : Array[Dependency]) -> Inventory

    Normalise declarations into a deterministic inventory.

    Declarations with the same package name and version are represented once. A canonical manifest path becomes the evidence location.

    match_version

    fn match_version(version : String, affected : String) -> FindingStatus

    Match a stable three-part numeric version against an affected range.

    Supported comparators are <, <=, >, >=, =, and bare exact versions. Space-separated terms form an intersection and || forms a union. Pre-release versions, build metadata, and malformed ranges return Uncomparable rather than being treated as vulnerable.

    parse_baseline

    fn parse_baseline(text : String) -> ScanBaseline raise

    Parse and strictly validate a schema-v1 or schema-v2 baseline snapshot.

    parse_database

    fn parse_database(text : String) -> AdvisoryDatabase raise

    parse_manifest

    fn parse_manifest(text : String, source~ : String) -> Array[Dependency] raise

    parse_moon_mod

    fn parse_moon_mod(text : String, source~ : String) -> Array[Dependency] raise

    Parse direct module dependencies from modern moon.mod syntax.

    Versions absent from a declaration are retained as empty so matching reports them as uncomparable instead of silently considering them safe.

    parse_moon_pkg

    fn parse_moon_pkg(text : String, source~ : String) -> Array[PackageImport] raise

    Parse package imports, aliases, and test scope from modern moon.pkg syntax.

    parse_policy

    fn parse_policy(text : String, today~ : String) -> ScanPolicy raise

    Parse and validate a policy using a UTC date supplied by the caller. Suppression expiries are inclusive: a rule remains active through expires_on.

    report_json

    fn report_json(report : ScanReport) -> String

    report_sarif

    fn report_sarif(report : ScanReport) -> String

    report_terminal

    fn report_terminal(report : ScanReport) -> String

    sbom_json

    fn sbom_json(dependencies : Array[Dependency]) -> String

    scan

    fn scan(dependencies : Array[Dependency], database : AdvisoryDatabase) -> ScanReport raise

    validate_baseline

    fn validate_baseline(baseline : ScanBaseline) -> Unit raise

    Validate a baseline created by a caller or deserializer.

    validate_database

    fn validate_database(database : AdvisoryDatabase) -> Unit raise

    Validate the supported advisory database schema and semantic constraints.

    validate_policy

    fn validate_policy(policy : ScanPolicy, today~ : String) -> Unit raise

    Validate a policy, including ISO date validity and expiration.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsCLIBuild queueStatistics

    © 2026 mooncakes.io