///|
pub(all) struct ProvenanceOptions {
builder_id : String
build_type : String
source_uri : String
revision : String
subject_digest : String
invocation_id : String
started_on : String
finished_on : String
reproducible : Bool
} derive(Eq, Debug)
///|
pub fn default_provenance_options() -> ProvenanceOptions {
{
builder_id: "https://github.com/liyun6666/moonseal",
build_type: "https://moonbitlang.com/build/v1",
source_uri: "",
revision: "",
subject_digest: "",
invocation_id: "",
started_on: "",
finished_on: "",
reproducible: true,
}
}
///|
fn provenance_subject(project : Project, digest : String) -> Json {
let digest_value : Json = if digest.is_empty() {
{}
} else {
{ "sha256": digest }
}
{
"name": if project.version.is_empty() {
"pkg:mooncakes/\{project.name}"
} else {
"pkg:mooncakes/\{project.name}@\{project.version}"
},
"digest": digest_value,
}
}
///|
fn provenance_external_parameters(project : Project) -> Json {
{
"module": project.name,
"version": project.version,
"license": normalize_license(project.license),
"target": "wasm",
}
}
///|
fn provenance_internal_parameters() -> Json {
{
"commands": [
"moon update", "moon check --target wasm", "moon test --target wasm", "moon build --target wasm --release",
],
}
}
///|
fn provenance_source(options : ProvenanceOptions) -> Json {
{
"uri": options.source_uri,
"digest": if options.revision.is_empty() {
{}
} else {
{ "gitCommit": options.revision }
},
}
}
///|
fn provenance_dependency_material(dependency : Dependency) -> Json {
{ "uri": dependency_purl(dependency), "digest": {} }
}
///|
fn provenance_dependencies(project : Project) -> Array[Json] {
project.dependencies.map(provenance_dependency_material)
}
///|
fn provenance_metadata(options : ProvenanceOptions) -> Json {
{
"invocationId": options.invocation_id,
"startedOn": options.started_on,
"finishedOn": options.finished_on,
}
}
///|
/// Create an in-toto Statement carrying a SLSA Provenance v1 predicate. The
/// caller supplies revision and digest values because the portable core does
/// not read Git state or hash build artifacts.
pub fn provenance(project : Project, options : ProvenanceOptions) -> Json {
let subjects : Array[Json] = [
provenance_subject(project, options.subject_digest),
]
let dependencies = provenance_dependencies(project)
{
"_type": "https://in-toto.io/Statement/v1",
"subject": subjects,
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": options.build_type,
"externalParameters": provenance_external_parameters(project),
"internalParameters": provenance_internal_parameters(),
"resolvedDependencies": [provenance_source(options), ..dependencies],
},
"runDetails": {
"builder": { "id": options.builder_id },
"metadata": provenance_metadata(options),
"byproducts": [],
},
"moonseal": {
"reproducible": options.reproducible,
"repository": project.repository,
"dependencyCount": project.dependencies.length(),
},
},
}
}
///|
pub fn render_provenance(
project : Project,
options : ProvenanceOptions,
indent? : Int = 2,
) -> String {
provenance(project, options).stringify(indent~)
}
///|
/// Validate data required for a useful, publishable provenance statement.
pub fn provenance_warnings(
project : Project,
options : ProvenanceOptions,
) -> Array[String] {
let warnings = []
if project.name.is_empty() {
warnings.push("Project name is required.")
}
if project.version.is_empty() {
warnings.push("Project version is required.")
}
if options.builder_id.is_empty() {
warnings.push("Builder identity is required.")
}
if options.build_type.is_empty() {
warnings.push("Build type is required.")
}
if options.source_uri.is_empty() {
warnings.push("Source repository URI is required.")
}
if options.revision.is_empty() {
warnings.push("Source revision is required.")
}
if options.subject_digest.is_empty() {
warnings.push("Artifact SHA-256 digest is recommended.")
} else if !provenance_is_sha256(options.subject_digest) {
warnings.push("Artifact digest must be 64 hexadecimal characters.")
}
if options.invocation_id.is_empty() {
warnings.push("Build invocation ID is recommended.")
}
for dependency in project.dependencies {
if dependency.version.is_empty() {
warnings.push(
"Dependency \{dependency.name} is unpinned in provenance materials.",
)
}
}
warnings
}
///|
fn provenance_is_hex(char : Char) -> Bool {
(char >= '0' && char <= '9') ||
(char >= 'a' && char <= 'f') ||
(char >= 'A' && char <= 'F')
}
///|
fn provenance_is_sha256(value : StringView) -> Bool {
guard value.length() == 64 else { return false }
for char in value {
guard provenance_is_hex(char) else { return false }
}
true
}
///|
pub fn render_provenance_warnings(
project : Project,
options : ProvenanceOptions,
) -> String {
let warnings = provenance_warnings(project, options)
let out = StringBuilder::new()
if warnings.is_empty() {
out.write_string("PROVENANCE READY Required provenance data is present.\n")
} else {
out.write_string("PROVENANCE INCOMPLETE \{warnings.length()} issue(s)\n\n")
for warning in warnings {
out.write_string("WARN \{warning}\n")
}
}
out.to_string()
}