///|
pub(all) struct Position {
line : Int
column : Int
} derive(Eq)
///|
pub(all) struct Dependency {
source : String
version : String?
as_name : String?
} derive(Eq)
///|
pub(all) struct ModuleManifest {
name : String
version : String
readme : String
repository : String
license : String
keywords : Array[String]
description : String
imports : Array[Dependency]
} derive(Eq)
///|
pub(all) struct PackageManifest {
imports : Array[Dependency]
} derive(Eq)
///|
pub(all) struct ProjectSnapshot {
module_manifest : ModuleManifest
packages : Array[PackageManifest]
} derive(Eq)
///|
pub(all) struct ProjectModel {
name : String
version : String
license : String
readme : String
repository : String
description : String
keywords : Array[String]
dependencies : Array[Dependency]
} derive(Eq)
///|
pub(all) struct Policy {
allowed_licenses : Array[String]
trusted_prefixes : Array[String]
require_readme : Bool
require_repository : Bool
require_license : Bool
require_description : Bool
min_description_length : Int
require_keywords : Bool
min_keywords : Int
require_valid_version : Bool
require_valid_repository : Bool
} derive(Eq)
///|
pub(all) struct Diagnostic {
code : String
message : String
severity : String
} derive(Eq)
///|
pub(all) struct AuditReport {
project : ProjectModel
diagnostics : Array[Diagnostic]
risk_count : Int
dependency_count : Int
} derive(Eq)
///|
pub(all) enum GuardError {
UnterminatedString(Position)
} derive(Eq)
///|
pub fn parse_mod(input : String) -> Result[ModuleManifest, GuardError] {
let name = match string_field(input, "name") {
Ok(value) => value
Err(err) => return Err(err)
}
let version = match string_field(input, "version") {
Ok(value) => value
Err(err) => return Err(err)
}
let readme = match string_field(input, "readme") {
Ok(value) => value
Err(err) => return Err(err)
}
let repository = match string_field(input, "repository") {
Ok(value) => value
Err(err) => return Err(err)
}
let license = match string_field(input, "license") {
Ok(value) => value
Err(err) => return Err(err)
}
let description = match string_field(input, "description") {
Ok(value) => value
Err(err) => return Err(err)
}
let keywords = match array_field(input, "keywords") {
Ok(values) => values
Err(err) => return Err(err)
}
let imports = match parse_imports(input) {
Ok(values) => values
Err(err) => return Err(err)
}
Ok({
name,
version,
readme,
repository,
license,
keywords,
description,
imports,
})
}
///|
pub fn parse_pkg(input : String) -> Result[PackageManifest, GuardError] {
match parse_imports(input) {
Ok(imports) => Ok({ imports, })
Err(err) => Err(err)
}
}
///|
pub fn project_from(
manifest : ModuleManifest,
packages : Array[PackageManifest],
) -> ProjectModel {
let dependencies : Array[Dependency] = []
for dep in manifest.imports {
dependencies.push(dep)
}
for pkg in packages {
for dep in pkg.imports {
dependencies.push(dep)
}
}
{
name: manifest.name,
version: manifest.version,
license: manifest.license,
readme: manifest.readme,
repository: manifest.repository,
description: manifest.description,
keywords: manifest.keywords,
dependencies,
}
}
///|
pub fn default_policy() -> Policy {
{
allowed_licenses: ["Apache-2.0", "MIT", "MulanPSL-2.0"],
trusted_prefixes: ["moonbitlang/", "wyc060514/"],
require_readme: true,
require_repository: true,
require_license: true,
require_description: true,
min_description_length: 10,
require_keywords: true,
min_keywords: 1,
require_valid_version: true,
require_valid_repository: true,
}
}
///|
pub fn strict_policy() -> Policy {
{
allowed_licenses: ["Apache-2.0", "MIT"],
trusted_prefixes: ["moonbitlang/"],
require_readme: true,
require_repository: true,
require_license: true,
require_description: true,
min_description_length: 30,
require_keywords: true,
min_keywords: 3,
require_valid_version: true,
require_valid_repository: true,
}
}
///|
pub fn permissive_policy() -> Policy {
{
allowed_licenses: [
"Apache-2.0", "MIT", "MulanPSL-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC",
"Unlicense",
],
trusted_prefixes: ["moonbitlang/", "wyc060514/", "github.com/"],
require_readme: false,
require_repository: false,
require_license: false,
require_description: false,
min_description_length: 0,
require_keywords: false,
min_keywords: 0,
require_valid_version: false,
require_valid_repository: false,
}
}
///|
pub fn scan_project(snapshot : ProjectSnapshot) -> AuditReport {
evaluate_policy(
project_from(snapshot.module_manifest, snapshot.packages),
default_policy(),
)
}
///|
/// Run a full audit: base policy plus every dependency and metadata check.
///
/// Combines `evaluate_policy` with the standalone dependency checks
/// (`check_unused_dependency`, `check_missing_versioned_dep`,
/// `check_version_consistency`, `check_version_conflicts`,
/// `check_self_dependency`) into a single report.
pub fn full_audit(
manifest : ModuleManifest,
packages : Array[PackageManifest],
) -> AuditReport {
let report = scan_project(ProjectSnapshot::{
module_manifest: manifest,
packages,
})
let diagnostics = report.diagnostics
append_diagnostics(diagnostics, check_unused_dependency(manifest, packages))
append_diagnostics(
diagnostics,
check_missing_versioned_dep(manifest, packages),
)
append_diagnostics(diagnostics, check_version_consistency(manifest))
append_diagnostics(diagnostics, check_version_conflicts(manifest, packages))
append_diagnostics(diagnostics, check_self_dependency(manifest))
{ ..report, diagnostics, risk_count: diagnostics.length() }
}
///|
fn append_diagnostics(
target : Array[Diagnostic],
extra : Array[Diagnostic],
) -> Unit {
for d in extra {
target.push(d)
}
}
///|
pub fn evaluate_policy(project : ProjectModel, policy : Policy) -> AuditReport {
let diagnostics : Array[Diagnostic] = []
if policy.require_readme && project.readme.length() == 0 {
diagnostics.push(diagnostic("missing-readme", "README is not declared"))
}
if policy.require_repository && project.repository.length() == 0 {
diagnostics.push(
diagnostic("missing-repository", "repository is not declared"),
)
}
if policy.require_valid_repository &&
project.repository.length() > 0 &&
!is_valid_repository_url(project.repository) {
diagnostics.push(
diagnostic(
"invalid-repository",
"repository '\{project.repository}' is not a valid URL",
),
)
}
if policy.require_license && project.license.length() == 0 {
diagnostics.push(diagnostic("missing-license", "license is not declared"))
} else if !contains_string(policy.allowed_licenses, project.license) {
diagnostics.push(
diagnostic(
"license-not-allowed",
"license \{project.license} is outside the allowed policy",
),
)
}
if policy.require_description &&
project.description.length() < policy.min_description_length {
diagnostics.push(
diagnostic(
"description-too-short",
"description is too short (\{project.description.length()} chars, min \{policy.min_description_length})",
),
)
}
if policy.require_keywords && project.keywords.length() < policy.min_keywords {
diagnostics.push(
diagnostic(
"missing-keywords",
"only \{project.keywords.length()} keywords, min \{policy.min_keywords} required",
),
)
}
if policy.require_valid_version && !validate_version(project.version) {
diagnostics.push(
diagnostic(
"invalid-version",
"version '\{project.version}' is not valid semver",
),
)
}
let seen : Array[String] = []
for dep in project.dependencies {
if contains_string(seen, dep.source) {
diagnostics.push(
diagnostic(
"duplicate-dependency",
"dependency \{dep.source} is duplicated",
),
)
} else {
if !is_trusted(dep.source, policy.trusted_prefixes) {
diagnostics.push(
diagnostic(
"unknown-dependency",
"dependency \{dep.source} is not trusted",
),
)
}
seen.push(dep.source)
}
}
{
project,
diagnostics,
risk_count: diagnostics.length(),
dependency_count: project.dependencies.length(),
}
}
///|
pub fn render_markdown(report : AuditReport) -> String {
let out = StringBuilder()
out.write_string("# MoonModGuard Audit Report\n\n")
out.write_string("- Project: \{report.project.name}\n")
out.write_string("- Dependencies: \{report.dependency_count}\n")
out.write_string("- Risks: \{report.risk_count}\n\n")
out.write_string("## Diagnostics\n\n")
if report.diagnostics.length() == 0 {
out.write_string("- No policy risks found.\n")
} else {
for item in report.diagnostics {
out.write_string("- [\{item.severity}] \{item.code}: \{item.message}\n")
}
}
out.to_string()
}
///|
pub fn render_json(report : AuditReport) -> String {
let out = StringBuilder()
out.write_string("{\n")
out.write_string(
" \"project\": \"" + escape_json(report.project.name) + "\",\n",
)
out.write_string(
" \"license\": \"" + escape_json(report.project.license) + "\",\n",
)
out.write_string(
" \"dependencies\": " + report.dependency_count.to_string() + ",\n",
)
out.write_string(" \"risks\": " + report.risk_count.to_string() + ",\n")
out.write_string(" \"diagnostics\": [\n")
render_json_diagnostics(out, report.diagnostics, 0)
out.write_string(" ]\n")
out.write_string("}\n")
out.to_string()
}
///|
fn render_json_diagnostics(
out : StringBuilder,
diags : Array[Diagnostic],
idx : Int,
) -> Unit {
if idx >= diags.length() {
return
}
let d = diags[idx]
if idx > 0 {
let _ = out.write_string(",\n")
}
let _ = out.write_string(
" {\"code\": \"" +
escape_json(d.code) +
"\", \"message\": \"" +
escape_json(d.message) +
"\", \"severity\": \"" +
escape_json(d.severity) +
"\"}",
)
let _ = render_json_diagnostics(out, diags, idx + 1)
}
///|
pub fn format_error(err : GuardError) -> String {
match err {
UnterminatedString(pos) =>
"line \{pos.line}, column \{pos.column}: unterminated string"
}
}
///|
fn diagnostic(code : String, message : String) -> Diagnostic {
{ code, message, severity: "error" }
}
///|
fn string_field(input : String, key : String) -> Result[String, GuardError] {
let lines = input.split("\n").to_array()
for i in 0.. return Ok(value)
Err(err) => return Err(err)
}
}
}
}
}
Ok("")
}
///|
fn array_field(
input : String,
key : String,
) -> Result[Array[String], GuardError] {
let values : Array[String] = []
let lines = input.split("\n").to_array()
let mut in_array = false
for i in 0..
for value in found {
values.push(value)
}
Err(err) => return Err(err)
}
if contains_char(chars, ']') {
in_array = false
}
}
}
Ok(values)
}
///|
fn parse_imports(input : String) -> Result[Array[Dependency], GuardError] {
let imports : Array[Dependency] = []
let lines = input.split("\n").to_array()
let mut in_import = false
for i in 0..
for dep in found {
imports.push(dep)
}
Err(err) => return Err(err)
}
if contains_char(chars, '}') {
in_import = false
}
}
}
Ok(imports)
}
///|
fn collect_imports(
chars : Array[Char],
line : Int,
start : Int,
) -> Result[Array[Dependency], GuardError] {
let deps : Array[Dependency] = []
let mut cursor = start
while cursor < chars.length() {
if chars[cursor] == '"' {
match read_string(chars, line, cursor) {
Ok((raw, next)) => {
cursor = next
let (source, version) = parse_dep_source(raw)
let as_name = read_as_name(chars, cursor)
deps.push({ source, version, as_name })
}
Err(err) => return Err(err)
}
} else {
cursor += 1
}
}
Ok(deps)
}
///|
/// Split a dependency string into source and optional version.
///
/// `"moonbitlang/x@0.4.46"` → `("moonbitlang/x", Some("0.4.46"))`
/// `"acme/dep"` → `("acme/dep", None)`
fn parse_dep_source(raw : String) -> (String, String?) {
let chars = raw.iter().to_array()
let mut last_at = -1
for i in 0.. 0 {
let after = raw[last_at + 1:raw.length()].to_owned()
if looks_like_version(after) {
return (raw[0:last_at].to_owned(), Some(after))
}
}
(raw, None)
}
///|
/// Quick check: does the string look like a version fragment (contains a digit)?
fn looks_like_version(s : String) -> Bool {
for c in s {
if c >= '0' && c <= '9' {
return true
}
}
false
}
///|
fn collect_strings(
chars : Array[Char],
line : Int,
start : Int,
) -> Result[Array[String], GuardError] {
let values : Array[String] = []
let mut cursor = start
while cursor < chars.length() {
if chars[cursor] == '"' {
match read_string(chars, line, cursor) {
Ok((value, next)) => {
values.push(value)
cursor = next
}
Err(err) => return Err(err)
}
} else {
cursor += 1
}
}
Ok(values)
}
///|
fn read_string(
chars : Array[Char],
line : Int,
start : Int,
) -> Result[(String, Int), GuardError] {
let value = StringBuilder()
let mut cursor = start + 1
while cursor < chars.length() {
if chars[cursor] == '\\' {
if cursor + 1 < chars.length() {
value.write_char(chars[cursor + 1])
cursor += 2
} else {
value.write_char(chars[cursor])
cursor += 1
}
} else if chars[cursor] == '"' {
return Ok((value.to_string(), cursor + 1))
} else {
value.write_char(chars[cursor])
cursor += 1
}
}
Err(UnterminatedString({ line, column: start + 1 }))
}
///|
fn read_as_name(chars : Array[Char], start : Int) -> String? {
let mut cursor = skip_spaces(chars, start)
if cursor >= chars.length() || chars[cursor] != '@' {
return None
}
cursor += 1
let name = StringBuilder()
while cursor < chars.length() && is_ident(chars[cursor]) {
name.write_char(chars[cursor])
cursor += 1
}
let value = name.to_string()
if value.length() == 0 {
None
} else {
Some(value)
}
}
///|
fn is_trusted(source : String, prefixes : Array[String]) -> Bool {
for prefix in prefixes {
if starts_with_string(source, prefix) {
return true
}
}
false
}
///|
fn contains_string(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
fn contains_char(values : Array[Char], target : Char) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
fn starts_with_key(chars : Array[Char], start : Int, key : String) -> Bool {
if !starts_with_word(chars, start, key) {
return false
}
let next = start + key.length()
next >= chars.length() || is_space(chars[next]) || chars[next] == '='
}
///|
fn starts_with_word(chars : Array[Char], start : Int, word : String) -> Bool {
let word_chars = word.iter().to_array()
if start + word_chars.length() > chars.length() {
return false
}
for i in 0.. Bool {
let chars = value.iter().to_array()
let prefix_chars = prefix.iter().to_array()
if prefix_chars.length() > chars.length() {
return false
}
for i in 0.. Int {
let mut cursor = 0
while cursor < chars.length() && is_space(chars[cursor]) {
cursor += 1
}
cursor
}
///|
fn skip_spaces(chars : Array[Char], start : Int) -> Int {
let mut cursor = start
while cursor < chars.length() && is_space(chars[cursor]) {
cursor += 1
}
cursor
}
///|
fn is_space(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\r'
}
///|
fn is_ident(c : Char) -> Bool {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '-'
}
///|
/// Convenience: parse mod text and package texts, then run the default policy audit.
pub fn audit_snapshot(
mod_text : String,
pkg_texts : Array[String],
) -> Result[AuditReport, GuardError] {
match parse_mod(mod_text) {
Ok(module_manifest) => {
let packages : Array[PackageManifest] = []
for text in pkg_texts {
match parse_pkg(text) {
Ok(pkg) => packages.push(pkg)
Err(err) => return Err(err)
}
}
Ok(scan_project(ProjectSnapshot::{ module_manifest, packages }))
}
Err(err) => Err(err)
}
}
///|
/// Validate a version string against full semver rules.
///
/// Supports: `MAJOR.MINOR.PATCH[-prerelease][+build]`
pub fn validate_version(version : String) -> Bool {
validate_semver(version)
}
///|
fn validate_semver(version : String) -> Bool {
let v = version
if v.length() == 0 {
return false
}
// Strip build metadata
let core_and_pre = match v.split_once("+") {
Some((core, build)) => {
if build.length() == 0 {
return false
}
core.to_owned()
}
None => v
}
// Split prerelease
let core = match core_and_pre.split_once("-") {
Some((c, pre)) => {
let pre_s = pre.to_owned()
if pre_s.length() == 0 || !is_valid_prerelease(pre_s) {
return false
}
c.to_owned()
}
None => core_and_pre
}
// Core: MAJOR.MINOR.PATCH
let parts = core.split(".").to_array()
if parts.length() != 3 {
return false
}
for part in parts {
if !is_non_negative_int(part.to_owned()) {
return false
}
}
// Leading zero check
for part in parts {
if part.length() > 1 && part[0] == '0' {
return false
}
}
true
}
///|
fn is_non_negative_int(s : String) -> Bool {
if s.length() == 0 {
return false
}
for c in s {
if c < '0' || c > '9' {
return false
}
}
true
}
///|
fn is_valid_prerelease(s : String) -> Bool {
if s.length() == 0 {
return false
}
for part in s.split(".") {
let part_str = part.to_owned()
if part_str.length() == 0 {
return false
}
for c in part_str {
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '-') {
return false
}
}
}
true
}
///|
fn escape_json(s : String) -> String {
let out = StringBuilder()
for c in s {
match c {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ =>
if c.to_int() < 0x20 {
out.write_string("\\u" + pad_hex(c.to_int(), 4))
} else {
out.write_char(c)
}
}
}
out.to_string()
}
///|
fn pad_hex(value : Int, width : Int) -> String {
let hex = "0123456789abcdef"
let mut result = ""
let mut v = value
for _ in 0.. AuditReport {
let report = evaluate_policy(project, policy)
report
}
///|
/// Check if a dependency declared in the module manifest is actually imported by any package.
pub fn check_unused_dependency(
manifest : ModuleManifest,
packages : Array[PackageManifest],
) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
let all_imports : Array[String] = []
for pkg in packages {
for dep in pkg.imports {
all_imports.push(dep.source)
}
}
for dep in manifest.imports {
if !contains_string(all_imports, dep.source) {
diagnostics.push(
diagnostic(
"unused-dependency",
"dependency \{dep.source} is declared but not imported by any package",
),
)
}
}
diagnostics
}
///|
/// Check that every package-level import has a corresponding versioned module dependency.
pub fn check_missing_versioned_dep(
manifest : ModuleManifest,
packages : Array[PackageManifest],
) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
let versioned : Array[(String, String)] = []
for dep in manifest.imports {
match dep.version {
Some(v) => versioned.push((dep.source, v))
None => ()
}
}
for pkg in packages {
for dep in pkg.imports {
let mut found = false
for i in 0.. Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for dep in manifest.imports {
match dep.version {
Some(v) =>
if !validate_version(v) {
diagnostics.push(
diagnostic(
"invalid-version",
"dependency \{dep.source} has invalid version \{v}",
),
)
}
None => ()
}
}
diagnostics
}
///|
/// Check for version conflicts: the same dependency source declared with
/// two or more different versions across the module and its packages.
pub fn check_version_conflicts(
manifest : ModuleManifest,
packages : Array[PackageManifest],
) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
let seen : Array[(String, String)] = []
let reported : Array[String] = []
let mut i = 0
while i < manifest.imports.length() {
let dep = manifest.imports[i]
match dep.version {
Some(v) => {
if has_conflicting_version(seen, dep.source, v) &&
!contains_string(reported, dep.source) {
diagnostics.push(
diagnostic(
"version-conflict",
"dependency \{dep.source} is declared with multiple conflicting versions",
),
)
reported.push(dep.source)
}
seen.push((dep.source, v))
}
None => ()
}
i = i + 1
}
for pkg in packages {
for dep in pkg.imports {
match dep.version {
Some(v) => {
if has_conflicting_version(seen, dep.source, v) &&
!contains_string(reported, dep.source) {
diagnostics.push(
diagnostic(
"version-conflict",
"dependency \{dep.source} is declared with multiple conflicting versions",
),
)
reported.push(dep.source)
}
seen.push((dep.source, v))
}
None => ()
}
}
}
diagnostics
}
///|
fn has_conflicting_version(
seen : Array[(String, String)],
source : String,
version : String,
) -> Bool {
for i in 0.. Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for dep in manifest.imports {
if dep.source == manifest.name {
diagnostics.push(
diagnostic("self-dependency", "module depends on itself: \{dep.source}"),
)
}
}
diagnostics
}
///|
/// Build a report from explicit fields (no parsing needed).
pub fn report_from(
name : String,
license : String,
readme : String,
repository : String,
description : String,
keywords : Array[String],
deps : Array[Dependency],
) -> AuditReport {
evaluate_policy(
{
name,
version: "0.1.0",
license,
readme,
repository,
description,
keywords,
dependencies: deps,
},
default_policy(),
)
}
///|
/// Return the diagnostic count for a severity level.
pub fn count_by_severity(
diagnostics : Array[Diagnostic],
severity : String,
) -> Int {
let mut count = 0
for d in diagnostics {
if d.severity == severity {
count = count + 1
}
}
count
}
// ------- Trust Levels (CSP-style grading) -------
///|
pub(all) enum TrustLevel {
Trusted
Allowed
Blocked
} derive(Eq)
///|
pub(all) struct TrustRule {
prefix : String
level : TrustLevel
} derive(Eq)
///|
pub(all) struct TrustPolicy {
rules : Array[TrustRule]
default_level : TrustLevel
} derive(Eq)
///|
pub fn TrustPolicy::new() -> TrustPolicy {
{ rules: [], default_level: Blocked }
}
///|
pub fn TrustPolicy::trust(self : TrustPolicy, prefix : String) -> TrustPolicy {
let new_rules = self.rules
new_rules.push({ prefix, level: Trusted })
{ ..self, rules: new_rules }
}
///|
pub fn TrustPolicy::allow(self : TrustPolicy, prefix : String) -> TrustPolicy {
let new_rules = self.rules
new_rules.push({ prefix, level: Allowed })
{ ..self, rules: new_rules }
}
///|
pub fn TrustPolicy::block(self : TrustPolicy, prefix : String) -> TrustPolicy {
let new_rules = self.rules
new_rules.push({ prefix, level: Blocked })
{ ..self, rules: new_rules }
}
///|
pub fn TrustPolicy::default_level(
self : TrustPolicy,
level : TrustLevel,
) -> TrustPolicy {
{ ..self, default_level: level }
}
///|
pub fn TrustPolicy::check(self : TrustPolicy, source : String) -> TrustLevel {
for rule in self.rules {
if starts_with_string(source, rule.prefix) {
return rule.level
}
}
self.default_level
}
// ------- Enhanced Policy -------
///|
pub(all) struct EnhancedPolicy {
trust_policy : TrustPolicy
require_readme : Bool
require_repository : Bool
require_license : Bool
require_description : Bool
min_description_length : Int
require_keywords : Bool
min_keywords : Int
require_version : Bool
max_dependencies : Int
} derive(Eq)
///|
pub fn EnhancedPolicy::default() -> EnhancedPolicy {
{
trust_policy: TrustPolicy::new().trust("moonbitlang/").trust("wyc060514/"),
require_readme: true,
require_repository: true,
require_license: true,
require_description: true,
min_description_length: 10,
require_keywords: true,
min_keywords: 1,
require_version: true,
max_dependencies: 50,
}
}
// ------- Enhanced Audit -------
///|
pub fn evaluate_enhanced(
project : ProjectModel,
policy : EnhancedPolicy,
) -> AuditReport {
let diagnostics : Array[Diagnostic] = []
// Metadata checks
if policy.require_readme && project.readme.length() == 0 {
diagnostics.push(diagnostic("missing-readme", "README is not declared"))
}
if policy.require_repository && project.repository.length() == 0 {
diagnostics.push(
diagnostic("missing-repository", "repository is not declared"),
)
}
if project.repository.length() > 0 &&
!is_valid_repository_url(project.repository) {
diagnostics.push(
diagnostic(
"invalid-repository",
"repository '\{project.repository}' is not a valid URL",
),
)
}
if policy.require_license && project.license.length() == 0 {
diagnostics.push(diagnostic("missing-license", "license is not declared"))
}
if policy.require_description &&
project.description.length() < policy.min_description_length {
diagnostics.push(
diagnostic(
"description-too-short",
"description is too short (\{project.description.length()} chars, min \{policy.min_description_length})",
),
)
}
if policy.require_keywords && project.keywords.length() < policy.min_keywords {
diagnostics.push(
diagnostic(
"missing-keywords",
"only \{project.keywords.length()} keywords, min \{policy.min_keywords} required",
),
)
}
if policy.require_version && !validate_version(project.version) {
diagnostics.push(
diagnostic(
"invalid-version",
"version '\{project.version}' is not valid semver",
),
)
}
if policy.max_dependencies > 0 &&
project.dependencies.length() > policy.max_dependencies {
diagnostics.push(
diagnostic(
"too-many-dependencies",
"\{project.dependencies.length()} dependencies exceeds limit of \{policy.max_dependencies}",
),
)
}
// Trust checks with graded levels
let seen : Array[String] = []
for dep in project.dependencies {
if contains_string(seen, dep.source) {
diagnostics.push(
diagnostic(
"duplicate-dependency",
"dependency \{dep.source} is duplicated",
),
)
} else {
let level = policy.trust_policy.check(dep.source)
match level {
Blocked =>
diagnostics.push(
diagnostic(
"blocked-dependency",
"dependency \{dep.source} is blocked by trust policy",
),
)
Allowed => ()
Trusted => ()
}
seen.push(dep.source)
}
}
{
project,
diagnostics,
risk_count: diagnostics.length(),
dependency_count: project.dependencies.length(),
}
}
///|
/// Check that the project name follows the `owner/package` convention.
pub fn validate_name(name : String) -> Bool {
let parts = name.split("/").to_array()
if parts.length() != 2 {
return false
}
let owner = parts[0]
let pkg = parts[1]
owner.length() > 0 && pkg.length() > 0
}
///|
/// Check that a repository field is a plausible version-control URL.
/// Accepts `https://`, `http://`, `git://`, `ssh://` and `git@host:path` forms.
pub fn is_valid_repository_url(repository : String) -> Bool {
let r = repository
if r.length() == 0 {
return false
}
if starts_with_string(r, "https://") ||
starts_with_string(r, "http://") ||
starts_with_string(r, "git://") ||
starts_with_string(r, "ssh://") {
return true
}
// git@github.com:owner/repo.git form
if starts_with_string(r, "git@") && r.contains(":") {
return true
}
false
}
///|
fn arrays_equal(a : Array[String], b : Array[String]) -> Bool {
if a.length() != b.length() {
return false
}
let mut i = 0
while i < a.length() {
if a[i] != b[i] {
return false
}
i = i + 1
}
true
}
///|
/// Check keyword quality: at least `min` meaningful keywords (length >= 3).
pub fn keyword_quality(keywords : Array[String], min : Int) -> Bool {
if keywords.length() < min {
return false
}
let mut count = 0
for kw in keywords {
if kw.length() >= 3 {
count = count + 1
}
}
count >= min
}
///|
/// Check for known-risky licenses (GPL, AGPL, etc.).
pub fn is_risky_license(license : String) -> Bool {
let risky = ["GPL-2.0", "GPL-3.0", "AGPL-3.0", "LGPL-2.1", "LGPL-3.0"]
contains_string(risky, license)
}
// ------- Enhanced Rendering -------
///|
/// Render a summary table of diagnostics grouped by severity.
pub fn render_summary(report : AuditReport) -> String {
let errors = count_by_severity(report.diagnostics, "error")
let warnings = count_by_severity(report.diagnostics, "warning")
let infos = count_by_severity(report.diagnostics, "info")
let out = StringBuilder()
out.write_string("| Severity | Count |\n")
out.write_string("|----------|-------|\n")
out.write_string("| error | \{errors} |\n")
out.write_string("| warning | \{warnings} |\n")
out.write_string("| info | \{infos} |\n")
out.to_string()
}
///|
/// Render a full audit report with a summary section.
pub fn render_full_report(report : AuditReport) -> String {
let out = StringBuilder()
out.write_string("# MoonModGuard Audit Report\n\n")
out.write_string("## Project\n\n")
out.write_string("- Name: \{report.project.name}\n")
out.write_string("- License: \{report.project.license}\n")
out.write_string("- Dependencies: \{report.dependency_count}\n")
out.write_string("- Risks: \{report.risk_count}\n\n")
out.write_string("## Summary\n\n")
out.write_string(render_summary(report))
out.write_string("\n\n## Diagnostics\n\n")
if report.diagnostics.length() == 0 {
out.write_string("- No policy risks found.\n")
} else {
for i in 0.. PositionedDiagnostic {
{ code, message, severity: "error", file, line }
}
///|
/// Render a positioned diagnostic as a GitHub Actions annotation.
pub fn render_annotation(d : PositionedDiagnostic) -> String {
"::\{d.severity} file=\{d.file},line=\{d.line}::\{d.code}: \{d.message}"
}
// ------- Batch Audit -------
///|
pub(all) struct AuditSummary {
total_projects : Int
total_diagnostics : Int
projects_clean : Int
projects_risky : Int
} derive(Eq)
///|
/// Run a batch audit on multiple projects and return a summary.
pub fn batch_audit(snapshots : Array[ProjectSnapshot]) -> AuditSummary {
let mut total_projects = 0
let mut total_diagnostics = 0
let mut projects_clean = 0
let mut projects_risky = 0
for snapshot in snapshots {
let report = scan_project(snapshot)
total_projects = total_projects + 1
total_diagnostics = total_diagnostics + report.risk_count
if report.risk_count == 0 {
projects_clean = projects_clean + 1
} else {
projects_risky = projects_risky + 1
}
}
{ total_projects, total_diagnostics, projects_clean, projects_risky }
}
///|
/// Render a batch audit result as a concise table.
pub fn render_batch_summary(summary : AuditSummary) -> String {
let out = StringBuilder()
out.write_string("# Batch Audit Summary\n\n")
out.write_string("| Metric | Value |\n")
out.write_string("|--------|-------|\n")
out.write_string("| Projects audited | \{summary.total_projects} |\n")
out.write_string("| Projects clean | \{summary.projects_clean} |\n")
out.write_string("| Projects risky | \{summary.projects_risky} |\n")
out.write_string("| Total diagnostics | \{summary.total_diagnostics} |\n")
out.to_string()
}
// ------- SARIF Renderer -------
///|
/// Render an audit report as SARIF v2.1.0 JSON for GitHub Code Scanning.
pub fn render_sarif(report : AuditReport, tool_name : String) -> String {
let out = StringBuilder()
out.write_string("{\n")
out.write_string(
" \"" +
"$schema" +
"\": \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json\",\n",
)
out.write_string(" \"version\": \"2.1.0\",\n")
out.write_string(" \"runs\": [{\n")
out.write_string(" \"tool\": {\n")
out.write_string(" \"driver\": {\n")
out.write_string(" \"name\": \"" + escape_json(tool_name) + "\",\n")
out.write_string(
" \"informationUri\": \"https://github.com/Noverberrain/MoonModGuard-MoonBit\",\n",
)
out.write_string(" \"rules\": [\n")
render_sarif_rules(out)
out.write_string(" ]\n")
out.write_string(" }\n")
out.write_string(" },\n")
out.write_string(" \"results\": [\n")
for i in 0.. 0 {
out.write_string(",\n")
}
let d = report.diagnostics[i]
out.write_string(" {\n")
out.write_string(" \"ruleId\": \"" + escape_json(d.code) + "\",\n")
out.write_string(
" \"level\": \"" + sarif_level(d.severity) + "\",\n",
)
out.write_string(" \"message\": {\n")
out.write_string(" \"text\": \"" + escape_json(d.message) + "\"\n")
out.write_string(" }\n")
out.write_string(" }")
}
out.write_string("\n ]\n")
out.write_string(" }]\n")
out.write_string("}\n")
out.to_string()
}
///|
fn render_sarif_rules(out : StringBuilder) -> Unit {
let rules = [
("missing-readme", "README is not declared"),
("missing-repository", "repository is not declared"),
("missing-license", "license is not declared"),
("license-not-allowed", "license is outside the allowed policy"),
("description-too-short", "description is too short"),
("missing-keywords", "insufficient keywords"),
("duplicate-dependency", "dependency is duplicated"),
("unknown-dependency", "dependency is not trusted"),
("blocked-dependency", "dependency is blocked by trust policy"),
("too-many-dependencies", "too many dependencies"),
("unused-dependency", "dependency is unused"),
("version-mismatch", "package import has no versioned module dependency"),
("invalid-version", "dependency has invalid version"),
]
for i in 0.. 0 {
out.write_string(",\n")
}
let (id, desc) = rules[i]
out.write_string(" {\n")
out.write_string(" \"id\": \"" + id + "\",\n")
out.write_string(
" \"shortDescription\": {\"text\": \"" +
escape_json(desc) +
"\"}\n",
)
out.write_string(" }")
}
}
///|
fn sarif_level(severity : String) -> String {
if severity == "error" {
"error"
} else if severity == "warning" {
"warning"
} else {
"note"
}
}
// ------- mooncake.yaml Consistency Check -------
///|
/// Parsed fields from a mooncake.yaml file.
pub(all) struct MooncakeManifest {
name : String
version : String
description : String
repository : String
license : String
keywords : Array[String]
} derive(Eq)
///|
/// Parse a mooncake.yaml text into a MooncakeManifest.
pub fn parse_mooncake(input : String) -> Result[MooncakeManifest, GuardError] {
let name = match string_field_yaml(input, "name") {
Ok(value) => value
Err(err) => return Err(err)
}
let version = match string_field_yaml(input, "version") {
Ok(value) => value
Err(err) => return Err(err)
}
let description = match string_field_yaml(input, "description") {
Ok(value) => value
Err(err) => return Err(err)
}
let repository = match string_field_yaml(input, "repository") {
Ok(value) => value
Err(err) => return Err(err)
}
let license = match string_field_yaml(input, "license") {
Ok(value) => value
Err(err) => return Err(err)
}
let keywords = match array_field_yaml(input, "keywords") {
Ok(values) => values
Err(err) => return Err(err)
}
Ok({ name, version, description, repository, license, keywords })
}
///|
/// Read a scalar value from YAML-style `key: value` pairs.
fn string_field_yaml(
input : String,
key : String,
) -> Result[String, GuardError] {
let lines = input.split("\n").to_array()
for i in 0.. Result[Array[String], GuardError] {
let values : Array[String] = []
let lines = input.split("\n").to_array()
for i in 0.. String {
let mut i = 0
while i < s.length() && (s[i] == ' ' || s[i] == '\t') {
i = i + 1
}
s[i:s.length()].to_owned()
}
///|
fn trim_right(s : String) -> String {
let mut i = s.length()
while i > 0 && (s[i - 1] == ' ' || s[i - 1] == '\t') {
i = i - 1
}
s[0:i].to_owned()
}
///|
/// Check consistency between moon.mod and mooncake.yaml manifests.
pub fn check_mooncake_consistency(
mod_manifest : ModuleManifest,
mooncake : MooncakeManifest,
) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
if mod_manifest.name != mooncake.name {
diagnostics.push(
diagnostic(
"mooncake-name-mismatch",
"moon.mod name '\{mod_manifest.name}' != mooncake.yaml name '\{mooncake.name}'",
),
)
}
if mod_manifest.version != mooncake.version {
diagnostics.push(
diagnostic(
"mooncake-version-mismatch",
"moon.mod version '\{mod_manifest.version}' != mooncake.yaml version '\{mooncake.version}'",
),
)
}
if mod_manifest.license != mooncake.license {
diagnostics.push(
diagnostic(
"mooncake-license-mismatch",
"moon.mod license '\{mod_manifest.license}' != mooncake.yaml license '\{mooncake.license}'",
),
)
}
if mod_manifest.repository != mooncake.repository {
diagnostics.push(
diagnostic(
"mooncake-repository-mismatch",
"moon.mod repository '\{mod_manifest.repository}' != mooncake.yaml repository '\{mooncake.repository}'",
),
)
}
if mod_manifest.description != mooncake.description {
diagnostics.push(
diagnostic(
"mooncake-description-mismatch", "moon.mod description != mooncake.yaml description",
),
)
}
if !arrays_equal(mod_manifest.keywords, mooncake.keywords) {
diagnostics.push(
diagnostic(
"mooncake-keywords-mismatch", "moon.mod keywords != mooncake.yaml keywords",
),
)
}
diagnostics
}
// ------- Workspace Scanner -------
///|
/// A discovered MoonBit package in a workspace.
pub(all) struct DiscoveredPackage {
dir : String
mod_text : String
pkg_text : String
mooncake_text : String?
} derive(Eq)
///|
/// Recursively discover MoonBit packages under a root directory.
///
/// Returns an array of DiscoveredPackage, one per directory that contains
/// a `moon.mod` or `moon.pkg` file. Directories named `_build`, `.mooncakes`,
/// `.git`, and `target` are skipped.
pub fn discover_packages(root : String) -> Array[DiscoveredPackage] raise {
let packages : Array[DiscoveredPackage] = []
discover_rec(root, packages)
packages
}
///|
fn discover_rec(
dir : String,
packages : Array[DiscoveredPackage],
) -> Unit raise {
let skip = ["_build", ".mooncakes", ".git", "target", ".moon"]
for s in skip {
if dir.has_suffix("/" + s) || dir.has_suffix("\\" + s) || dir == s {
return
}
}
let dir_name = if dir.has_suffix("/") || dir.has_suffix("\\") {
dir
} else {
dir + "/"
}
let mut mod_text : String? = None
let mut pkg_text : String? = None
let mut mooncake_text : String? = None
let entries = @fs.read_dir(dir)
for entry in entries {
let full = dir_name + entry
if @fs.is_file(full) {
if entry == "moon.mod" {
mod_text = Some(@fs.read_file_to_string(full))
} else if entry == "moon.pkg" {
pkg_text = Some(@fs.read_file_to_string(full))
} else if entry == "mooncake.yaml" {
mooncake_text = Some(@fs.read_file_to_string(full))
}
} else if @fs.is_dir(full) {
discover_rec(full, packages)
}
}
if mod_text is Some(_) || pkg_text is Some(_) {
packages.push({
dir: dir_name,
mod_text: mod_text.unwrap_or_default(),
pkg_text: pkg_text.unwrap_or_default(),
mooncake_text,
})
}
}
///|
/// Batch-audit all packages discovered under a root directory.
pub fn audit_workspace(root : String) -> AuditSummary raise {
let packages = discover_packages(root)
let snapshots : Array[ProjectSnapshot] = []
for pkg in packages {
match parse_mod(pkg.mod_text) {
Ok(mod_manifest) => {
let pkg_manifests : Array[PackageManifest] = []
match parse_pkg(pkg.pkg_text) {
Ok(pm) => pkg_manifests.push(pm)
Err(_) => ()
}
snapshots.push(ProjectSnapshot::{
module_manifest: mod_manifest,
packages: pkg_manifests,
})
}
Err(_) => ()
}
}
batch_audit(snapshots)
}