diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6909cc..f002742 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false + fetch-depth: 0 - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: global-json-file: global.json diff --git a/README.md b/README.md index 2b781de..bc60e70 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,7 @@ The repository check enforces the [project licence boundary](docs/licence-bounda for the application, core library, tests and C# tooling. Every build also checks the effective MSBuild declarations and local project references. +The same tool enforces [source provenance](docs/provenance.md): complete file +classification, immutable reuse records, full notices and portable-archive checks. + Read [CONTRIBUTING](CONTRIBUTING.md), [security reporting](SECURITY.md) and the [code of conduct](CODE_OF_CONDUCT.md). ArcNotes remains **AGPL-3.0-only**; see [LICENSE](LICENSE) and [third-party notices](THIRD_PARTY_NOTICES.md). diff --git a/docs/provenance.md b/docs/provenance.md new file mode 100644 index 0000000..7bafcd2 --- /dev/null +++ b/docs/provenance.md @@ -0,0 +1,58 @@ +# Source provenance (WP00.03) + +The [accepted Design profile](https://github.com/ArcForges/ArcForges-Design/blob/5322d698a1b650a52a5a139d986dd85b00b48581/docs/assurance/reference-coverage-and-provenance.md) +governs the current ArcNotes repository. The retired initialization repository is +historical lineage, not a source/build prerequisite. Desktop UI remains native +Avalonia/Skia. This change adds no browser UI or tooling runtime dependency. + +Before reusing material, complete `eng/provenance/template.json` as a record in +`eng/provenance/records/.json`. The ten fields identify the exact source, +commit and paths, file-level licence, attribution, target, disposition, oracle, +NOTICE and lifetime. Generated material also identifies each generator and input; +temporary material names its removal trigger and owner. The Licensing and +Provenance Owner reviews actual evidence within the closed decision table in +`eng/policy/reuse-policy.json`. A maintainer-authorized review may exercise that +role; an `approved` string alone cannot replace review. Record unresolved conflicts +under `eng/provenance/conflicts` and resolve them before accepting the material. + +`eng/provenance/files.json` accounts for every tracked file, including tracked +ignored files, and every non-ignored new file during local checks. Reused targets +have exact record bindings and SHA-256 values; all other files are explicitly +classified as current first-party material. Review must also detect copied material +inserted into an existing file: an inventory check cannot establish authorship. +The original records reconcile existing legal documents honestly and do not claim +that approval preceded their historical introduction. + +Used records remain immutable, even after retirement. A changed source, target, +intent or obligation requires a new revision with `supersedes`; retain its earlier +records and update only the active binding. CI reads its trusted event base and +requires the complete comparison history. A retained external file cannot silently +become first-party material. The explanatory template is never an approved record. + +Run the existing C# checks and tests from `CONTRIBUTING.md`. After completing and +reviewing new records/inventory, regenerate the deterministic summary with: + +```sh +dotnet run --project eng/ArcForges.Repository -- provenance-notice +dotnet run --project eng/ArcForges.Repository -- check +``` + +The checker rejects missing/blank fields, unclassified files, unknown/prohibited +licences, missing or changed targets, escaping/linked paths, altered history, +broken supersession and missing notices. Its C# adaptation records the Apache +Contracts source at `18a970c67c463f1971ca05773b80f31a1b1ba1a7`; full original terms +and changed-file attribution remain in the repository. This source/build tool is +absent from the Native AOT application. No sibling source is consumed during builds. + +Portable staging requires committed, audited source. It retains the root licence, +six exact upstream legal documents, the generated package source summary and the +actual source-audit receipt. Packing and independent release verification read the +real ZIP/tar entries, compare full legal bytes and require a passing receipt for +the candidate's clean source commit. Existing dependency notices remain intact; +the summary does not replace them. The same gates run on all five native hosts. + +Locked dependencies, Native AOT, native UI actions, real Cloud requests, failure +cases and exact public release assets retain their existing gates. Source or +licence checks do not establish product completeness, OS trust signing or later +commercial readiness. Each contribution records its own observed runtime and +publication evidence after full review and successful CI. diff --git a/eng/ArcForges.Repository/Program.cs b/eng/ArcForges.Repository/Program.cs index 2a37f37..5a6c41d 100644 --- a/eng/ArcForges.Repository/Program.cs +++ b/eng/ArcForges.Repository/Program.cs @@ -26,6 +26,7 @@ public static async Task Main(string[] args) await Run("git", ["config", "--worktree", "core.hooksPath", ".githooks"]); break; case ["check"]: await Check(); break; + case ["provenance-notice"]: await ProvenancePolicy.Check(Directory.GetCurrentDirectory(), writeNotice: true); break; case ["version"]: var generatedVersion = Version(Environment.GetEnvironmentVariable("GITHUB_RUN_NUMBER") ?? "0", Environment.GetEnvironmentVariable("GITHUB_RUN_ATTEMPT") ?? "1"); Console.WriteLine(generatedVersion); @@ -38,7 +39,7 @@ public static async Task Main(string[] args) Directory.CreateDirectory("artifacts/evidence"); await Run(Executable(rid), ["--smoke-live", "--evidence", Path.GetFullPath($"artifacts/evidence/{rid}.json")]); break; - case ["pack", var rid, var version, var commit]: Pack(rid, version, commit); break; + case ["pack", var rid, var version, var commit]: await Pack(rid, version, commit); break; case ["verify", var path, var version, var commit]: var manifests = Directory.GetFiles(path, "manifest.json", SearchOption.AllDirectories); if (manifests.Length != Rids.Length) throw new InvalidOperationException("Expected all five native candidates."); @@ -50,7 +51,7 @@ public static async Task Main(string[] args) } Console.WriteLine("Verified all five immutable native candidates."); break; - default: throw new ArgumentException("Use hooks, check, version, prepare RID VERSION, smoke RID, pack RID VERSION COMMIT, or verify DIRECTORY VERSION COMMIT."); + default: throw new ArgumentException("Use hooks, check, provenance-notice, version, prepare RID VERSION, smoke RID, pack RID VERSION COMMIT, or verify DIRECTORY VERSION COMMIT."); } return 0; } @@ -86,6 +87,7 @@ private static async Task Prepare(string rid, string version) { ValidateRid(rid); ValidateVersion(version); + await CheckSourceForDistribution(); var source = Path.GetFullPath($"artifacts/publish/{rid}"); var stage = Stage(rid); if (Directory.Exists(stage)) throw new InvalidOperationException("Staging already exists; use a fresh worktree/output directory."); @@ -100,6 +102,10 @@ private static async Task Prepare(string rid, string version) } foreach (var name in new[] { "LICENSE", "THIRD_PARTY_NOTICES.md", "README.md" }) File.Copy(name, Path.Combine(stage, name)); WriteDependencyNotices(stage); + var requiredNotices = ProvenancePolicy.PackageNotices(Directory.GetCurrentDirectory()); + File.WriteAllBytes(Path.Combine(stage, "notices/source-provenance.txt"), requiredNotices["notices/source-provenance.txt"]); + ProvenancePolicy.VerifyPackageNotices(requiredNotices, path => File.Exists(Path.Combine(stage, path)) ? File.ReadAllBytes(Path.Combine(stage, path)) : null); + File.Copy("artifacts/evidence/provenance.json", Path.Combine(stage, "notices/provenance-source.json")); if (!File.Exists(Executable(rid))) throw new InvalidOperationException("Published executable is missing."); if (rid.StartsWith("osx-", StringComparison.Ordinal)) { @@ -154,11 +160,21 @@ private static void WriteDependencyNotices(string stage) File.Copy(file, Path.Combine(stage, "notices/upstream", Path.GetFileName(file))); } - private static void Pack(string rid, string version, string commit) + private static async Task CheckSourceForDistribution() + { + await ProvenancePolicy.Check(Directory.GetCurrentDirectory()); + if ((await Capture("git", ["status", "--porcelain"])).Length != 0) + throw new InvalidOperationException("Commit reviewed source before producing a source-bound portable candidate."); + } + + private static async Task Pack(string rid, string version, string commit) { ValidateRid(rid); ValidateVersion(version); if (commit.Length != 40 || !commit.All(char.IsAsciiHexDigit)) throw new ArgumentException("Expected a full Git commit."); + await CheckSourceForDistribution(); + if ((await Capture("git", ["rev-parse", "HEAD"])).Trim() != commit) + throw new InvalidOperationException("Pack source differs from the candidate revision."); var evidence = $"artifacts/evidence/{rid}.json"; using var smoke = JsonDocument.Parse(File.ReadAllText(evidence)); ValidateSmoke(smoke.RootElement, rid, version, commit); @@ -175,6 +191,7 @@ private static void Pack(string rid, string version, string commit) using var gzip = new GZipStream(file, CompressionLevel.Optimal); TarFile.CreateFromDirectory(Stage(rid), gzip, false); } + VerifyArchiveNotices(archive, commit, Directory.GetCurrentDirectory()); File.Copy(evidence, Path.Combine(folder, "smoke.json")); File.Copy(Path.ChangeExtension(evidence, ".png"), Path.Combine(folder, "screen.png")); var manifest = new Candidate(rid, version, commit, name, Hash(archive), Hash(evidence)); @@ -183,7 +200,7 @@ private static void Pack(string rid, string version, string commit) Console.WriteLine($"Packed verified {rid}: {name}"); } - public static string VerifyCandidate(string manifestPath, string version, string commit) + public static string VerifyCandidate(string manifestPath, string version, string commit, string? sourceRoot = null) { var manifest = JsonSerializer.Deserialize(File.ReadAllText(manifestPath), Json) ?? throw new InvalidOperationException("Missing manifest."); ValidateRid(manifest.Rid); @@ -200,9 +217,61 @@ public static string VerifyCandidate(string manifestPath, string version, string throw new InvalidOperationException("Download checksum mismatch."); using var evidence = JsonDocument.Parse(File.ReadAllText(smoke)); ValidateSmoke(evidence.RootElement, manifest.Rid, version, commit); + VerifyArchiveNotices(archive, commit, sourceRoot ?? Directory.GetCurrentDirectory()); return manifest.Rid; } + private static void VerifyArchiveNotices(string archive, string commit, string sourceRoot) + { + var expected = ProvenancePolicy.PackageNotices(sourceRoot); + const string receiptName = "notices/provenance-source.json"; + var selected = new Dictionary(StringComparer.Ordinal); + var members = new HashSet(StringComparer.OrdinalIgnoreCase); + void Retain(string path, Stream? stream) + { + while (path.StartsWith("./", StringComparison.Ordinal)) path = path[2..]; + path = path.TrimEnd('/'); + if (path.Length == 0 || path == ".") return; + if (path.StartsWith('/') || path.IndexOfAny(['\\', ':', '\0']) >= 0 || path.Split('/').Any(p => p is "" or "." or "..")) + throw new InvalidOperationException("Escaping archive member: " + path); + if (!members.Add(path)) throw new InvalidOperationException("Duplicate or case-colliding archive member: " + path); + if (!expected.ContainsKey(path) && path != receiptName) return; + if (stream is null) throw new InvalidOperationException("Required notice is not a regular archive file: " + path); + using var memory = new MemoryStream(); + stream.CopyTo(memory); + if (!selected.TryAdd(path, memory.ToArray())) throw new InvalidOperationException("Duplicate archive notice: " + path); + } + if (archive.EndsWith(".zip", StringComparison.Ordinal)) + { + using var zip = ZipFile.OpenRead(archive); + foreach (var entry in zip.Entries) + { + if (((entry.ExternalAttributes >> 16) & 0xF000) == 0xA000) + throw new InvalidOperationException("Linked ZIP member: " + entry.FullName); + using var stream = entry.Open(); + Retain(entry.FullName, stream); + } + } + else + { + using var file = File.OpenRead(archive); + using var gzip = new GZipStream(file, CompressionMode.Decompress); + using var tar = new TarReader(gzip); + while (tar.GetNextEntry() is { } entry) + { + if (entry.EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) + throw new InvalidOperationException("Linked tar member: " + entry.Name); + Retain(entry.Name, entry.DataStream); + } + } + ProvenancePolicy.VerifyPackageNotices(expected, path => selected.GetValueOrDefault(path)); + if (!selected.TryGetValue(receiptName, out var receipt)) throw new InvalidOperationException("Missing candidate source provenance."); + using var report = JsonDocument.Parse(receipt); + if (report.RootElement.GetProperty("sourceCommit").GetString() != commit || + report.RootElement.GetProperty("dirty").GetBoolean() || report.RootElement.GetProperty("result").GetString() != "passed") + throw new InvalidOperationException("Candidate provenance does not match clean reviewed source."); + } + private static void ValidateSmoke(JsonElement smoke, string rid, string version, string commit) { if (!smoke.GetProperty("success").GetBoolean() || !smoke.GetProperty("nativeAot").GetBoolean() || @@ -223,6 +292,7 @@ private static string Hash(string path) private static async Task Check() { await Run("git", ["diff", "--check"]); + await ProvenancePolicy.Check(Directory.GetCurrentDirectory()); using var upstream = JsonDocument.Parse(File.ReadAllText("third-party/sources.json")); foreach (var entry in upstream.RootElement.EnumerateArray()) { diff --git a/eng/ArcForges.Repository/ProvenancePolicy.cs b/eng/ArcForges.Repository/ProvenancePolicy.cs new file mode 100644 index 0000000..2a320ed --- /dev/null +++ b/eng/ArcForges.Repository/ProvenancePolicy.cs @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Adapted from ArcForges Contracts eng/check_provenance.py (Apache-2.0). +// C# rewrite and integration changes: ArcForges. See eng/third-party/LICENSE.Contracts.txt. +using System.Diagnostics; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace ArcForges.Repository; + +public static class ProvenancePolicy +{ + public const string Owner = "ArcNotes"; + public const string Inventory = "eng/provenance/files.json"; + public const string Summary = "eng/provenance/NOTICE.txt"; + public const string Policy = "eng/policy/reuse-policy.json"; + private const string Store = "eng/provenance/records/"; + private const string RecordFields = "schemaVersion id kind sourceRepository sourceCommit sourcePaths licence attribution targets artifactTargets disposition verification notice lifetime generation review supersedes"; + private static readonly UTF8Encoding Utf8 = new(false, true); + private static readonly JsonSerializerOptions Json = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private static readonly Dictionary Licences = new(StringComparer.Ordinal) + { + ["Apache-2.0"] = "permissive", + ["MIT"] = "permissive", + ["BSD-2-Clause"] = "permissive", + ["BSD-3-Clause"] = "permissive", + ["ISC"] = "permissive", + ["Apache-2.0 WITH LLVM-exception"] = "permissive", + ["Apache-2.0 AND BSD-3-Clause"] = "permissive", + ["Apache-2.0 AND MIT"] = "permissive", + ["AGPL-3.0-only"] = "agpl-compatible", + ["GPL-2.0-only"] = "gpl-only", + ["GPL-3.0-only"] = "gpl-only", + ["NOASSERTION"] = "unclear", + ["EPL-1.0"] = "incompatible" + }; + + private static void Require(bool value, string message) + { + if (!value) throw new InvalidOperationException(message); + } + + private static string Text(JsonElement value) + { + Require(value.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(value.GetString()), "Blank or non-string provenance field."); + return value.GetString()!; + } + + private static void Fields(JsonElement value, string fields) + { + Require(value.ValueKind == JsonValueKind.Object, "Expected provenance object."); + var names = value.EnumerateObject().Select(p => p.Name).ToArray(); + Require(names.Length == fields.Split(' ').Length && names.ToHashSet(StringComparer.Ordinal).SetEquals(fields.Split(' ')), "Missing, duplicate or unknown provenance fields: " + fields); + } + + private static JsonElement Parse(byte[] bytes) + { + using var document = JsonDocument.Parse(Utf8.GetString(bytes)); + static void Unique(JsonElement item) + { + if (item.ValueKind == JsonValueKind.Object) + { + var names = new HashSet(StringComparer.Ordinal); + foreach (var field in item.EnumerateObject()) + { + Require(names.Add(field.Name), "Duplicate JSON field: " + field.Name); + Unique(field.Value); + } + } + else if (item.ValueKind == JsonValueKind.Array) + foreach (var entry in item.EnumerateArray()) Unique(entry); + } + Unique(document.RootElement); + return document.RootElement.Clone(); + } + + private static string Relative(string value) + { + Require(!string.IsNullOrWhiteSpace(value) && !value.StartsWith('/') && value.IndexOfAny(['\\', ':', '*', '?', '\0']) < 0 && + value.Split('/').All(p => p is not ("" or "." or "..")), "Nonliteral or escaping provenance path: " + value); + return value; + } + + public static byte[] Read(string root, string relative) + { + root = Path.GetFullPath(root); + var target = Path.Combine(root, Relative(relative)); + for (var current = target; current != root; current = Path.GetDirectoryName(current)!) + { + Require(File.Exists(current) || Directory.Exists(current), "Missing provenance input: " + relative); + Require((File.GetAttributes(current) & FileAttributes.ReparsePoint) == 0, "Linked provenance input: " + relative); + } + Require(File.Exists(target), "Provenance input is not a file: " + relative); + return File.ReadAllBytes(target); + } + + private static string Hash(byte[] bytes) => Convert.ToHexStringLower(SHA256.HashData(bytes)); + private static byte[] Lf(byte[] bytes) => Utf8.GetBytes(Utf8.GetString(bytes).Replace("\r\n", "\n", StringComparison.Ordinal)); + private static void Digest(string value, bool commit = false) => Require((value.Length == 64 || commit && value.Length == 40) && value.All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f'), "Expected full immutable hash."); + private static void Id(string value) => Require(Regex.IsMatch(value, @"\A[a-z0-9]+(?:-[a-z0-9]+)*-r[1-9][0-9]*\z", RegexOptions.CultureInvariant), "Invalid provenance record ID."); + private static void Repository(string value) => Require(Uri.TryCreate(value, UriKind.Absolute, out var uri) && uri.Scheme == "https" && uri.Host.Length > 0 && uri.UserInfo.Length == 0 && uri.Query.Length == 0 && uri.Fragment.Length == 0 && uri.AbsolutePath.Trim('/').Length > 0 && !value.EndsWith('/') && value.Split('/').All(p => p is not ("." or "..")), "Expected canonical HTTPS source repository."); + private static JsonElement[] Array(JsonElement value, bool empty = false) + { + Require(value.ValueKind == JsonValueKind.Array, "Expected provenance array."); + var result = value.EnumerateArray().ToArray(); + Require(empty || result.Length > 0, "Empty required provenance array."); + return result; + } + private static string[] Strings(JsonElement value, Action? validate = null, bool empty = false) + { + var result = Array(value, empty).Select(Text).ToArray(); + foreach (var item in result) validate?.Invoke(item); + Require(result.Distinct(StringComparer.Ordinal).Count() == result.Length, "Duplicate provenance array entry."); + return result; + } + private static void Paths(JsonElement value, bool empty = false) => Strings(value, p => Relative(p), empty); + private static void Evidence(JsonElement value) + { + foreach (var item in Array(value)) + { + Fields(item, "path sha256 finding"); + Relative(Text(item.GetProperty("path"))); + Digest(Text(item.GetProperty("sha256"))); + Text(item.GetProperty("finding")); + } + } + private static string Category(string expression) + { + Require(Licences.ContainsKey(expression), "Unknown provenance SPDX expression."); + return Licences[expression]; + } + private static void Compatible(string expression) => Require(Category(expression) is "permissive" or "agpl-compatible", "Prohibited implementation reuse."); + private static void Source(JsonElement item) + { + Fields(item, "repository commit paths spdx evidence"); + Repository(Text(item.GetProperty("repository"))); + Digest(Text(item.GetProperty("commit")), true); + Paths(item.GetProperty("paths")); + Compatible(Text(item.GetProperty("spdx"))); + Evidence(item.GetProperty("evidence")); + } + private static void CheckPolicy(JsonElement value) + { + Fields(value, "schemaVersion repository licenceBoundary authority decisions licences"); + Require(value.GetProperty("schemaVersion").GetRawText() == "1" && Text(value.GetProperty("repository")) == Owner && Text(value.GetProperty("licenceBoundary")) == "AGPL", "Incorrect provenance owner."); + var authority = value.GetProperty("authority"); + Fields(authority, "repository commit path"); + Require(Text(authority.GetProperty("repository")) == "https://github.com/ArcForges/ArcForges-Design" && Text(authority.GetProperty("path")) == "docs/assurance/reference-coverage-and-provenance.md", "Incorrect provenance authority."); + Digest(Text(authority.GetProperty("commit")), true); + var decisions = value.GetProperty("decisions"); + Fields(decisions, "permissive agpl-compatible gpl-only unclear incompatible"); + foreach (var row in decisions.EnumerateObject()) + { + Fields(row.Value, "AGPL Apache"); + Require(Text(row.Value.GetProperty("AGPL")) == (row.Name == "permissive" ? "audit" : row.Name == "agpl-compatible" ? "exact-review" : "prohibited") && + Text(row.Value.GetProperty("Apache")) == (row.Name == "permissive" ? "audit" : "prohibited"), "Changed closed licence decision table."); + } + var licences = value.GetProperty("licences"); + Require(licences.ValueKind == JsonValueKind.Object && licences.EnumerateObject().Count() == Licences.Count, "Changed closed licence expressions."); + foreach (var pair in licences.EnumerateObject()) Require(Category(pair.Name) == Text(pair.Value), "Changed licence category."); + } + private static void Record(JsonElement item) + { + Fields(item, RecordFields); + Require(item.GetProperty("schemaVersion").GetRawText() == "1", "Incorrect record schema."); + var id = Text(item.GetProperty("id")); + Id(id); + var kind = Text(item.GetProperty("kind")); + Require(kind is "source" or "patch" or "generated" or "legal-text", "Unknown material kind."); + Repository(Text(item.GetProperty("sourceRepository"))); + Digest(Text(item.GetProperty("sourceCommit")), true); + Paths(item.GetProperty("sourcePaths")); + var licence = item.GetProperty("licence"); + Fields(licence, "spdx category evidence scope copyingPermission"); + var expression = Text(licence.GetProperty("spdx")); + Require(Category(expression) == Text(licence.GetProperty("category")), "Mismatched licence category."); + Evidence(licence.GetProperty("evidence")); + Text(licence.GetProperty("scope")); + if (kind == "legal-text") + { + Text(licence.GetProperty("copyingPermission")); + Require(expression != "NOASSERTION", "Unknown-origin legal text."); + } + else + { + Require(licence.GetProperty("copyingPermission").ValueKind == JsonValueKind.Null, "Legal-document permission used for implementation."); + Compatible(expression); + } + Strings(item.GetProperty("attribution")); + var disposition = Text(item.GetProperty("disposition")); + Require(disposition is "Copy" or "Rewrite" or "Improve" or "Replace" or "Reference Only" or "Drop", "Unknown disposition."); + var targets = new HashSet(StringComparer.Ordinal); + foreach (var target in Array(item.GetProperty("targets"), true)) + { + Fields(target, "path sha256 normalization"); + var path = Relative(Text(target.GetProperty("path"))); + Require(targets.Add(path) && !path.StartsWith("eng/provenance/", StringComparison.Ordinal) && path != Policy, "Duplicate or self-attesting target."); + Digest(Text(target.GetProperty("sha256"))); + Require(Text(target.GetProperty("normalization")) is "lf" or "raw", "Unknown target normalization."); + if (kind == "legal-text") + { + var name = Path.GetFileName(path).ToLowerInvariant(); + Require(new[] { "license", "licence", "copying", "notice" }.Any(prefix => name.StartsWith(prefix, StringComparison.Ordinal)) || path.StartsWith("third-party/", StringComparison.Ordinal) && name.EndsWith(".txt", StringComparison.Ordinal), "Legal text targets implementation."); + } + } + var artifactKeys = new HashSet<(string, string, string)>(); + foreach (var target in Array(item.GetProperty("artifactTargets"), true)) + { + Fields(target, "project package kind profile sha256"); + Require(artifactKeys.Add((Text(target.GetProperty("project")), Text(target.GetProperty("package")), Text(target.GetProperty("kind")))), "Duplicate artifact target."); + Require(Relative(Text(target.GetProperty("profile"))).StartsWith("eng/provenance/artifact-profiles/", StringComparison.Ordinal), "Artifact profile outside provenance store."); + Digest(Text(target.GetProperty("sha256"))); + } + Require(artifactKeys.Count == 0 || kind == "generated", "Artifact requires generator and input positions."); + Require(disposition is "Reference Only" or "Drop" ? targets.Count + artifactKeys.Count == 0 : targets.Count + artifactKeys.Count > 0, "Disposition does not match targets."); + var oracle = item.GetProperty("verification"); + Fields(oracle, "kind command expected artifacts"); + Require(Text(oracle.GetProperty("kind")) is "byte-match" or "regeneration" or "transformation", "Unknown verification oracle."); + Text(oracle.GetProperty("command")); + Text(oracle.GetProperty("expected")); + foreach (var artifact in Array(oracle.GetProperty("artifacts"), true)) + { + Fields(artifact, "url sha256 members"); + Repository(Text(artifact.GetProperty("url"))); + Digest(Text(artifact.GetProperty("sha256"))); + Paths(artifact.GetProperty("members")); + } + var notice = item.GetProperty("notice"); + Fields(notice, "required text files distribution reason"); + Require(notice.GetProperty("required").ValueKind is JsonValueKind.True or JsonValueKind.False, "NOTICE requirement must be boolean."); + Text(notice.GetProperty("text")); + Paths(notice.GetProperty("files"), !notice.GetProperty("required").GetBoolean()); + Require(Text(notice.GetProperty("distribution")) is "source" or "packages" or "documentation", "Unknown notice distribution."); + Text(notice.GetProperty("reason")); + var lifetime = item.GetProperty("lifetime"); + Fields(lifetime, "status owner removalTrigger"); + Text(lifetime.GetProperty("owner")); + Require(Text(lifetime.GetProperty("status")) is "temporary" or "permanent", "Unknown lifetime."); + if (Text(lifetime.GetProperty("status")) == "temporary") Text(lifetime.GetProperty("removalTrigger")); + else Require(lifetime.GetProperty("removalTrigger").ValueKind == JsonValueKind.Null, "Permanent record has temporary removal trigger."); + var generation = item.GetProperty("generation"); + if (kind == "generated") + { + Fields(generation, "generators inputs command outputSpdx"); + foreach (var role in new[] { "generators", "inputs" }) + foreach (var input in Array(generation.GetProperty(role))) Source(input); + Text(generation.GetProperty("command")); + Compatible(Text(generation.GetProperty("outputSpdx"))); + } + else Require(generation.ValueKind == JsonValueKind.Null, "Non-generated record carries generation metadata."); + var review = item.GetProperty("review"); + Fields(review, "owner reviewer reviewedOn decision rationale baselineCommit reconciliation"); + Require(Text(review.GetProperty("owner")) == "Licensing and Provenance Owner" && Text(review.GetProperty("decision")) == "approved", "Unapproved disposition."); + Text(review.GetProperty("reviewer")); + Text(review.GetProperty("rationale")); + Require(DateOnly.TryParseExact(Text(review.GetProperty("reviewedOn")), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out _), "Invalid review date."); + Digest(Text(review.GetProperty("baselineCommit")), true); + Require(review.GetProperty("reconciliation").ValueKind is JsonValueKind.True or JsonValueKind.False, "Missing reconciliation classification."); + if (item.GetProperty("supersedes").ValueKind != JsonValueKind.Null) + { + Id(Text(item.GetProperty("supersedes"))); + Require(Text(item.GetProperty("supersedes")) != id, "Self-superseding record."); + } + } + + private static string Notice(IReadOnlyDictionary records, IEnumerable active, bool packages = false) + { + var lines = new List { "ArcForges source provenance notices", "", "Generated from reviewed active records. Original licence files and dependency notices remain authoritative.", "" }; + foreach (var id in active.Order(StringComparer.Ordinal)) + { + var item = records[id]; + var notice = item.GetProperty("notice"); + if (packages && Text(notice.GetProperty("distribution")) != "packages") continue; + lines.AddRange([id, $"Source: {Text(item.GetProperty("sourceRepository"))} @ {Text(item.GetProperty("sourceCommit"))}", + "Material licence: " + Text(item.GetProperty("licence").GetProperty("spdx")), "Notice scope: " + Text(notice.GetProperty("distribution"))]); + lines.AddRange(Strings(item.GetProperty("attribution"))); + lines.Add(Text(notice.GetProperty("text"))); + lines.Add(""); + } + return string.Join('\n', lines).TrimEnd() + "\n"; + } + + public static ProvenanceResult Validate(string root, IEnumerable inventory, IReadOnlyDictionary history, JsonElement? oldInventory = null, bool writeNotice = false) + { + var files = inventory.Distinct(StringComparer.Ordinal).ToHashSet(StringComparer.Ordinal); + Require(files.Count == files.Distinct(StringComparer.OrdinalIgnoreCase).Count(), "Case-colliding inventory."); + foreach (var file in files) Read(root, file); + CheckPolicy(Parse(Read(root, Policy))); + var template = Parse(Read(root, "eng/provenance/template.json")); + Fields(template, "schemaVersion instructions example"); + Require(template.GetProperty("schemaVersion").GetRawText() == "1", "Invalid template schema."); + Text(template.GetProperty("instructions")); + Fields(template.GetProperty("example"), RecordFields); + var inv = Parse(Read(root, Inventory)); + Fields(inv, "schemaVersion repository firstParty reused artifacts"); + Require(inv.GetProperty("schemaVersion").GetRawText() == "1" && Text(inv.GetProperty("repository")) == Owner, "Invalid inventory owner."); + var authored = Strings(inv.GetProperty("firstParty"), p => Relative(p)).ToHashSet(StringComparer.Ordinal); + var reused = inv.GetProperty("reused").EnumerateObject().ToDictionary(p => Relative(p.Name), p => Text(p.Value), StringComparer.Ordinal); + foreach (var id in reused.Values) Id(id); + var artifacts = Strings(inv.GetProperty("artifacts"), Id, true).ToHashSet(StringComparer.Ordinal); + Require(!authored.Overlaps(reused.Keys) && files.SetEquals(authored.Concat(reused.Keys)), "Unclassified, conflicting or stale inventory files."); + var records = new Dictionary(StringComparer.Ordinal); + foreach (var file in files) + { + if (file.StartsWith("eng/provenance/conflicts/", StringComparison.Ordinal)) + { + var conflict = Parse(Read(root, file)); + Fields(conflict, "id material evidence boundary owner requiredDecision status resolution"); + foreach (var property in conflict.EnumerateObject()) Text(property.Value); + Require(Text(conflict.GetProperty("status")) == "resolved", "Unresolved provenance conflict."); + } + if (!file.StartsWith(Store, StringComparison.Ordinal)) continue; + var item = Parse(Read(root, file)); + Record(item); + var id = Text(item.GetProperty("id")); + Require(file == Store + id + ".json" && records.TryAdd(id, item), "Record path/ID mismatch or duplicate record."); + } + foreach (var original in history) + Require(files.Contains(original.Key) && Lf(Read(root, original.Key)).SequenceEqual(Lf(original.Value)), "Used record changed or removed: " + original.Key); + string? Parent(string id) => records[id].GetProperty("supersedes").ValueKind == JsonValueKind.Null ? null : Text(records[id].GetProperty("supersedes")); + foreach (var id in records.Keys) + { + var seen = new HashSet(StringComparer.Ordinal) { id }; + for (var parent = Parent(id); parent is not null; parent = Parent(parent)) + Require(records.ContainsKey(parent) && seen.Add(parent), "Missing or cyclic superseded record."); + } + var active = reused.Values.Concat(artifacts).ToHashSet(StringComparer.Ordinal); + Require(active.Count > 0, "At least one real record must be in use."); + foreach (var id in active) + { + Require(records.ContainsKey(id), "Missing provenance record: " + id); + var item = records[id]; + var targets = Array(item.GetProperty("targets"), true); + Require(targets.Select(t => Text(t.GetProperty("path"))).ToHashSet(StringComparer.Ordinal).SetEquals(reused.Where(p => p.Value == id).Select(p => p.Key)), "Active record targets do not match complete inventory bindings."); + var artifactTargets = Array(item.GetProperty("artifactTargets"), true); + Require((artifactTargets.Length > 0) == artifacts.Contains(id), "Artifact record is not explicitly registered."); + foreach (var target in artifactTargets) + { + var path = Text(target.GetProperty("profile")); + Require(files.Contains(path) && Hash(Lf(Read(root, path))) == Text(target.GetProperty("sha256")), "Untracked or changed artifact profile."); + } + foreach (var target in targets) + { + var bytes = Read(root, Text(target.GetProperty("path"))); + if (Text(target.GetProperty("normalization")) == "lf") bytes = Lf(bytes); + Require(Hash(bytes) == Text(target.GetProperty("sha256")), "Recorded target bytes changed: " + Text(target.GetProperty("path"))); + } + foreach (var path in Strings(item.GetProperty("notice").GetProperty("files"), empty: true)) + { + Require(files.Contains(path), "Untracked required notice: " + path); + Read(root, path); + } + } + bool Descends(string current, string old) + { + for (string? id = current; id is not null; id = Parent(id)) + if (id == old) return true; + return false; + } + if (oldInventory is { } prior) + { + foreach (var old in prior.GetProperty("reused").EnumerateObject()) + if (files.Contains(old.Name)) Require(reused.TryGetValue(old.Name, out var current) && Descends(current, Text(old.Value)), "Retained external file lost provenance: " + old.Name); + foreach (var oldId in Strings(prior.GetProperty("artifacts"), empty: true)) + foreach (var target in Array(Parse(history[Store + oldId + ".json"]).GetProperty("artifactTargets"))) + { + var replacements = artifacts.Where(id => Array(records[id].GetProperty("artifactTargets")).Any(row => new[] { "project", "package", "kind" }.All(key => Text(row.GetProperty(key)) == Text(target.GetProperty(key))))).ToArray(); + Require(replacements.Length == 1 && Descends(replacements[0], oldId), "Active artifact silently removed or reclassified."); + } + } + var expected = Utf8.GetBytes(Notice(records, active)); + if (writeNotice) File.WriteAllBytes(Path.Combine(root, Summary), expected); + Require(Lf(Read(root, Summary)).SequenceEqual(expected), "Stale generated provenance NOTICE."); + return new ProvenanceResult("passed", Owner, files.Count, reused.Count, records.Count, active.Order(StringComparer.Ordinal).ToArray(), Hash(expected)); + } + + private static async Task Git(string root, params string[] args) + { + var info = new ProcessStartInfo("git") { WorkingDirectory = root, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; + foreach (var arg in args) info.ArgumentList.Add(arg); + using var process = Process.Start(info) ?? throw new InvalidOperationException("Git could not start."); + var output = process.StandardOutput.ReadToEndAsync(); + var error = process.StandardError.ReadToEndAsync(); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + try { await process.WaitForExitAsync(timeout.Token); } + catch (OperationCanceledException) { process.Kill(true); throw new InvalidOperationException("Provenance Git lookup timed out."); } + var text = await output; + var diagnostic = await error; + Require(process.ExitCode == 0, "Provenance history unavailable: " + diagnostic); + return text; + } + + public static string? EventBase(string? eventName, JsonElement? data) => eventName switch + { + "pull_request" => Text(data!.Value.GetProperty("pull_request").GetProperty("base").GetProperty("sha")), + "push" => Text(data!.Value.GetProperty("before")), + "merge_group" => Text(data!.Value.GetProperty("merge_group").GetProperty("base_sha")), + _ => null + }; + + public static async Task Check(string root, bool writeNotice = false) + { + var eventPath = Environment.GetEnvironmentVariable("GITHUB_EVENT_PATH"); + var eventName = Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME"); + var comparison = EventBase(eventName, eventPath is null ? null : Parse(File.ReadAllBytes(eventPath))); + comparison ??= (await Git(root, "rev-parse", "--abbrev-ref", "HEAD")).Trim() == "main" ? "HEAD" : "origin/main"; + comparison = (await Git(root, "rev-parse", "--verify", comparison + "^{commit}")).Trim(); + Digest(comparison, true); + var files = (await Git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard")).Split('\0', StringSplitOptions.RemoveEmptyEntries); + var history = new Dictionary(StringComparer.Ordinal); + foreach (var path in (await Git(root, "ls-tree", "-r", "--name-only", comparison, "--", Store)).Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + history.Add(path, Utf8.GetBytes(await Git(root, "show", comparison + ":" + path))); + var priorExists = (await Git(root, "ls-tree", "-r", "--name-only", comparison, "--", Inventory)).Trim().Length > 0; + JsonElement? prior = priorExists ? Parse(Utf8.GetBytes(await Git(root, "show", comparison + ":" + Inventory))) : null; + var result = Validate(root, files, history, prior, writeNotice); + var directory = Path.Combine(root, "artifacts/evidence"); + Directory.CreateDirectory(directory); + var report = new + { + result.Result, + result.Repository, + result.Files, + result.ReusedFiles, + result.Records, + result.ActiveRecords, + result.NoticeSha256, + sourceCommit = (await Git(root, "rev-parse", "HEAD")).Trim(), + comparisonCommit = comparison, + dirty = (await Git(root, "status", "--porcelain")).Length != 0 + }; + await File.WriteAllTextAsync(Path.Combine(directory, "provenance.json"), JsonSerializer.Serialize(report, Json) + "\n"); + Console.WriteLine($"Provenance passed: {result.Files} files, {result.ReusedFiles} reused files, {result.Records} immutable records."); + } + + public static IReadOnlyDictionary PackageNotices(string root) + { + var inv = Parse(Read(root, Inventory)); + var ids = inv.GetProperty("reused").EnumerateObject().Select(p => Text(p.Value)).Concat(Strings(inv.GetProperty("artifacts"), empty: true)).ToHashSet(StringComparer.Ordinal); + var records = ids.ToDictionary(id => id, id => Parse(Read(root, Store + id + ".json")), StringComparer.Ordinal); + var expected = new Dictionary(StringComparer.Ordinal) { ["notices/source-provenance.txt"] = Utf8.GetBytes(Notice(records, ids, true)) }; + foreach (var record in records.Values.Where(r => Text(r.GetProperty("notice").GetProperty("distribution")) == "packages")) + foreach (var target in Array(record.GetProperty("targets"), true)) + { + var path = Text(target.GetProperty("path")); + var destination = path == "LICENSE" ? path : path.StartsWith("third-party/", StringComparison.Ordinal) ? "notices/upstream/" + Path.GetFileName(path) : throw new InvalidOperationException("Unassigned distribution notice: " + path); + var bytes = Read(root, path); + var normalized = Text(target.GetProperty("normalization")) == "lf" ? Lf(bytes) : bytes; + Require(Hash(normalized) == Text(target.GetProperty("sha256")), "Changed package notice source: " + path); + Require(expected.TryAdd(destination, bytes), "Duplicate distribution notice binding."); + } + return expected; + } + + public static void VerifyPackageNotices(IReadOnlyDictionary expected, Func read) + { + foreach (var item in expected) + Require(read(item.Key) is { } actual && actual.SequenceEqual(item.Value), "Missing or changed package provenance notice: " + item.Key); + } +} + +public sealed record ProvenanceResult(string Result, string Repository, int Files, int ReusedFiles, int Records, string[] ActiveRecords, string NoticeSha256); diff --git a/eng/policy/reuse-policy.json b/eng/policy/reuse-policy.json new file mode 100644 index 0000000..f7a5e5a --- /dev/null +++ b/eng/policy/reuse-policy.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": 1, + "repository": "ArcNotes", + "licenceBoundary": "AGPL", + "authority": { + "repository": "https://github.com/ArcForges/ArcForges-Design", + "commit": "5322d698a1b650a52a5a139d986dd85b00b48581", + "path": "docs/assurance/reference-coverage-and-provenance.md" + }, + "decisions": { + "permissive": { + "AGPL": "audit", + "Apache": "audit" + }, + "agpl-compatible": { + "AGPL": "exact-review", + "Apache": "prohibited" + }, + "gpl-only": { + "AGPL": "prohibited", + "Apache": "prohibited" + }, + "unclear": { + "AGPL": "prohibited", + "Apache": "prohibited" + }, + "incompatible": { + "AGPL": "prohibited", + "Apache": "prohibited" + } + }, + "licences": { + "Apache-2.0": "permissive", + "MIT": "permissive", + "BSD-2-Clause": "permissive", + "BSD-3-Clause": "permissive", + "ISC": "permissive", + "Apache-2.0 WITH LLVM-exception": "permissive", + "Apache-2.0 AND BSD-3-Clause": "permissive", + "AGPL-3.0-only": "agpl-compatible", + "GPL-2.0-only": "gpl-only", + "GPL-3.0-only": "gpl-only", + "NOASSERTION": "unclear", + "EPL-1.0": "incompatible", + "Apache-2.0 AND MIT": "permissive" + } +} diff --git a/eng/provenance/NOTICE.txt b/eng/provenance/NOTICE.txt new file mode 100644 index 0000000..3dddb1a --- /dev/null +++ b/eng/provenance/NOTICE.txt @@ -0,0 +1,78 @@ +ArcForges source provenance notices + +Generated from reviewed active records. Original licence files and dependency notices remain authoritative. + +canonical-agpl-legal-r1 +Source: https://github.com/github/choosealicense.com @ 58267f8f2c5c0099810849cfd7677f52ae0c0eb3 +Material licence: AGPL-3.0-only +Notice scope: packages +Copyright (C) 2007 Free Software Foundation, Inc. +Copyright (C) 2007 Free Software Foundation, Inc. + +contracts-apache-legal-r1 +Source: https://github.com/ArcForges/Contracts @ 18a970c67c463f1971ca05773b80f31a1b1ba1a7 +Material licence: Apache-2.0 +Notice scope: source +Apache License, Version 2.0. Copyright notices remain in the individual source records. +Apache License, Version 2.0. Copyright notices remain in the individual source records. + +contracts-provenance-csharp-r2 +Source: https://github.com/ArcForges/Contracts @ 18a970c67c463f1971ca05773b80f31a1b1ba1a7 +Material licence: Apache-2.0 +Notice scope: source +Copyright ArcForges contributors. Original provenance checker: Apache-2.0; full text in eng/third-party/LICENSE.Contracts.txt. +ArcForges modifications: C# rewrite, existing repository-tool integration and portable archive notice validation. +Copyright ArcForges contributors. Original provenance checker: Apache-2.0; full text in eng/third-party/LICENSE.Contracts.txt. +ArcForges modifications: C# rewrite, existing repository-tool integration and portable archive notice validation. + +upstream-avalonia-legal-r1 +Source: https://github.com/AvaloniaUI/Avalonia @ d3c867a9e2de379249b03dbeb3495bd7f076a81a +Material licence: MIT +Notice scope: packages +Copyright (c) AvaloniaUI OÜ. All Rights Reserved. +Copyright (c) AvaloniaUI OÜ. All Rights Reserved. +Retain the complete licence, permissions and disclaimer in third-party/Avalonia.LICENSE.txt. + +upstream-dotnet-legal-r1 +Source: https://github.com/dotnet/dotnet @ 95017c711e6afc1085133d440e42b4bd78155701 +Material licence: MIT +Notice scope: packages +Copyright (c) .NET Foundation and Contributors. All rights reserved. +Copyright (c) .NET Foundation and Contributors. All rights reserved. +Retain the complete licence, permissions and disclaimer in third-party/dotnet.LICENSE.txt. + +upstream-grpc-legal-r1 +Source: https://github.com/grpc/grpc-dotnet @ 4301104498e53898a452e8fb2fea6c0b1492b755 +Material licence: Apache-2.0 +Notice scope: packages +Apache License, Version 2.0. gRPC for .NET contributors; full source licence is retained. +Apache License, Version 2.0. gRPC for .NET contributors; full source licence is retained. +Retain the complete licence, permissions and disclaimer in third-party/gRPC.LICENSE.txt. + +upstream-microcom-legal-r1 +Source: https://github.com/kekekeks/MicroCom @ 76785efcafd91b5902fd19dd11145f6dd655b7b4 +Material licence: MIT +Notice scope: packages +Copyright (c) 2021 Nikita Tsukanov +Copyright (c) 2021 Nikita Tsukanov +Retain the complete licence, permissions and disclaimer in third-party/MicroCom.LICENSE.txt. + +upstream-protobuf-legal-r1 +Source: https://github.com/protocolbuffers/protobuf @ f377bfefc5e2cfab68b816903c25b23e091c439d +Material licence: BSD-3-Clause +Notice scope: packages +Copyright 2008 Google Inc. All rights reserved. +Copyright 2008 Google Inc. All rights reserved. +Retain the complete licence, permissions and disclaimer in third-party/Protobuf.LICENSE.txt. + +upstream-tmds-dbus-legal-r1 +Source: https://github.com/tmds/Tmds.DBus @ b4a7fed0b878f74cb54f7cca84d2889af4e596ba +Material licence: MIT +Notice scope: packages +Copyright 2006 Alp Toker +Copyright 2010 Other Contributors +Copyright 2016 Tom Deseyn +Copyright 2006 Alp Toker +Copyright 2010 Other Contributors +Copyright 2016 Tom Deseyn +Retain the complete licence, permissions and disclaimer in third-party/Tmds.DBus.LICENSE.txt. diff --git a/eng/provenance/files.json b/eng/provenance/files.json new file mode 100644 index 0000000..5df44e0 --- /dev/null +++ b/eng/provenance/files.json @@ -0,0 +1,86 @@ +{ + "schemaVersion": 1, + "repository": "ArcNotes", + "firstParty": [ + ".editorconfig", + ".gitattributes", + ".githooks/pre-commit", + ".githooks/pre-push", + ".github/CODEOWNERS", + ".github/ISSUE_TEMPLATE/bug.yml", + ".github/ISSUE_TEMPLATE/config.yml", + ".github/ISSUE_TEMPLATE/feature.yml", + ".github/dependabot.yml", + ".github/pull_request_template.md", + ".github/workflows/ci.yml", + ".gitignore", + "AGENTS.md", + "ArcNotes.slnx", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "Directory.Build.props", + "Directory.Build.targets", + "Directory.Packages.props", + "NuGet.Config", + "README.md", + "SECURITY.md", + "THIRD_PARTY_NOTICES.md", + "docs/bootstrap-plan.md", + "docs/development.md", + "docs/licence-boundary.md", + "docs/provenance.md", + "docs/release-notes.md", + "docs/releasing.md", + "eng/ArcForges.Repository/ArcForges.Repository.csproj", + "eng/ArcForges.Repository/LicencePolicy.cs", + "eng/ArcForges.Repository/Program.cs", + "eng/ArcForges.Repository/packages.lock.json", + "eng/policy/licence-boundary.json", + "eng/policy/reuse-policy.json", + "eng/provenance/NOTICE.txt", + "eng/provenance/files.json", + "eng/provenance/records/canonical-agpl-legal-r1.json", + "eng/provenance/records/contracts-apache-legal-r1.json", + "eng/provenance/records/contracts-provenance-csharp-r1.json", + "eng/provenance/records/contracts-provenance-csharp-r2.json", + "eng/provenance/records/upstream-avalonia-legal-r1.json", + "eng/provenance/records/upstream-dotnet-legal-r1.json", + "eng/provenance/records/upstream-grpc-legal-r1.json", + "eng/provenance/records/upstream-microcom-legal-r1.json", + "eng/provenance/records/upstream-protobuf-legal-r1.json", + "eng/provenance/records/upstream-tmds-dbus-legal-r1.json", + "eng/provenance/template.json", + "global.json", + "src/ArcForges.ArcNotes.Core/ArcForges.ArcNotes.Core.csproj", + "src/ArcForges.ArcNotes.Core/CloudHelloClient.cs", + "src/ArcForges.ArcNotes.Core/HelloViewModel.cs", + "src/ArcForges.ArcNotes.Core/packages.lock.json", + "src/ArcForges.ArcNotes/ArcForges.ArcNotes.csproj", + "src/ArcForges.ArcNotes/ArcNotesApp.cs", + "src/ArcForges.ArcNotes/LiveSmoke.cs", + "src/ArcForges.ArcNotes/MainWindow.cs", + "src/ArcForges.ArcNotes/Program.cs", + "src/ArcForges.ArcNotes/app.manifest", + "src/ArcForges.ArcNotes/packages.lock.json", + "tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj", + "tests/ArcForges.ArcNotes.Tests/CandidateTests.cs", + "tests/ArcForges.ArcNotes.Tests/HelloStateTests.cs", + "tests/ArcForges.ArcNotes.Tests/LicencePolicyTests.cs", + "tests/ArcForges.ArcNotes.Tests/ProvenancePolicyTests.cs", + "tests/ArcForges.ArcNotes.Tests/TransportTests.cs", + "tests/ArcForges.ArcNotes.Tests/packages.lock.json", + "third-party/sources.json" + ], + "reused": { + "LICENSE": "canonical-agpl-legal-r1", + "eng/third-party/LICENSE.Contracts.txt": "contracts-apache-legal-r1", + "eng/ArcForges.Repository/ProvenancePolicy.cs": "contracts-provenance-csharp-r2", + "third-party/Avalonia.LICENSE.txt": "upstream-avalonia-legal-r1", + "third-party/dotnet.LICENSE.txt": "upstream-dotnet-legal-r1", + "third-party/gRPC.LICENSE.txt": "upstream-grpc-legal-r1", + "third-party/MicroCom.LICENSE.txt": "upstream-microcom-legal-r1", + "third-party/Protobuf.LICENSE.txt": "upstream-protobuf-legal-r1", + "third-party/Tmds.DBus.LICENSE.txt": "upstream-tmds-dbus-legal-r1" + }, + "artifacts": [] +} diff --git a/eng/provenance/records/canonical-agpl-legal-r1.json b/eng/provenance/records/canonical-agpl-legal-r1.json new file mode 100644 index 0000000..6eb58e7 --- /dev/null +++ b/eng/provenance/records/canonical-agpl-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "canonical-agpl-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/github/choosealicense.com", + "sourceCommit": "58267f8f2c5c0099810849cfd7677f52ae0c0eb3", + "sourcePaths": [ + "_licenses/agpl-3.0.txt" + ], + "licence": { + "spdx": "AGPL-3.0-only", + "category": "agpl-compatible", + "evidence": [ + { + "path": "_licenses/agpl-3.0.txt", + "sha256": "2a1fda78234b71502e2cf8035021b5592f5eba7e8107228aca4d95bba645e397", + "finding": "Canonical licence body, after YAML front matter; exact LF-normalized body equals the retained LICENSE. No unknown historical download origin is asserted." + } + ], + "scope": "Reconcile the unchanged root AGPL-3.0-only legal text with the pinned authoritative licence template body.", + "copyingPermission": "Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed." + }, + "attribution": [ + "Copyright (C) 2007 Free Software Foundation, Inc." + ], + "targets": [ + { + "path": "LICENSE", + "sha256": "8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef", + "normalization": "lf" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "transformation", + "command": "dotnet run --project eng/ArcForges.Repository -- check", + "expected": "Reconcile the unchanged root AGPL-3.0-only legal text with the pinned authoritative licence template body. Exact reviewed target digests and immutable history must match.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright (C) 2007 Free Software Foundation, Inc.", + "files": [ + "LICENSE", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reconcile the unchanged root AGPL-3.0-only legal text with the pinned authoritative licence template body.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/records/contracts-apache-legal-r1.json b/eng/provenance/records/contracts-apache-legal-r1.json new file mode 100644 index 0000000..eb1cc10 --- /dev/null +++ b/eng/provenance/records/contracts-apache-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "contracts-apache-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/ArcForges/Contracts", + "sourceCommit": "18a970c67c463f1971ca05773b80f31a1b1ba1a7", + "sourcePaths": [ + "LICENSE" + ], + "licence": { + "spdx": "Apache-2.0", + "category": "permissive", + "evidence": [ + { + "path": "LICENSE", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "finding": "Verbatim Apache 2.0 licence text accompanying the explicitly Apache Python source." + } + ], + "scope": "Retain the complete Apache terms accompanying the rewritten source checker; this document does not add a runtime dependency.", + "copyingPermission": "Verbatim reproduction of the licence accompanying Apache-2.0 redistribution, including section 4(a)." + }, + "attribution": [ + "Apache License, Version 2.0. Copyright notices remain in the individual source records." + ], + "targets": [ + { + "path": "eng/third-party/LICENSE.Contracts.txt", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "normalization": "lf" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "dotnet run --project eng/ArcForges.Repository -- check", + "expected": "Retain the full legal terms of the copied checker and tests; this is a legal document, not implementation. Exact reviewed target digests and immutable history must match.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Apache License, Version 2.0. Copyright notices remain in the individual source records.", + "files": [ + "eng/third-party/LICENSE.Contracts.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "source", + "reason": "Source provenance summary supplements the retained original full legal text." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Retain the complete Apache terms accompanying the rewritten source checker; this document does not add a runtime dependency.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": false + }, + "supersedes": null +} diff --git a/eng/provenance/records/contracts-provenance-csharp-r1.json b/eng/provenance/records/contracts-provenance-csharp-r1.json new file mode 100644 index 0000000..bd4f158 --- /dev/null +++ b/eng/provenance/records/contracts-provenance-csharp-r1.json @@ -0,0 +1,68 @@ +{ + "schemaVersion": 1, + "id": "contracts-provenance-csharp-r1", + "kind": "source", + "sourceRepository": "https://github.com/ArcForges/Contracts", + "sourceCommit": "18a970c67c463f1971ca05773b80f31a1b1ba1a7", + "sourcePaths": [ + "eng/check_provenance.py" + ], + "licence": { + "spdx": "Apache-2.0", + "category": "permissive", + "evidence": [ + { + "path": "eng/check_provenance.py", + "sha256": "c9e888d9845bd479c36439922c26038f29807214740ad7c467dfb3bb414a3024", + "finding": "The individual Python file declares SPDX-License-Identifier: Apache-2.0." + } + ], + "scope": "Rewrite the reviewed Apache provenance checker as C# in the existing AGPL repository tool. Preserve the Apache source attribution and full licence; the derivative implementation is distributed under AGPL-3.0-only. Add no Python/Node runtime requirement and no sibling source build dependency.", + "copyingPermission": null + }, + "attribution": [ + "Copyright ArcForges contributors. Original provenance checker: Apache-2.0; full text in eng/third-party/LICENSE.Contracts.txt.", + "ArcForges modifications: C# rewrite, existing repository-tool integration and portable archive notice validation." + ], + "targets": [ + { + "path": "eng/ArcForges.Repository/ProvenancePolicy.cs", + "sha256": "7dc2f731e11658161e2e34eb5915d606cdb866cb6d908b118300911389c7a0dc", + "normalization": "lf" + } + ], + "artifactTargets": [], + "disposition": "Rewrite", + "verification": { + "kind": "transformation", + "command": "dotnet run --project eng/ArcForges.Repository -- check", + "expected": "Independent C# positive/negative provenance tests, real Git inventory and history checks, deterministic NOTICE agreement with the Apache checker, and immutable reviewed target hash.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright ArcForges contributors. Original provenance checker: Apache-2.0; full text in eng/third-party/LICENSE.Contracts.txt.\nArcForges modifications: C# rewrite, existing repository-tool integration and portable archive notice validation.", + "files": [ + "eng/third-party/LICENSE.Contracts.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "source", + "reason": "This is source/build tooling; it is absent from the Native AOT application. Retain the original legal text and changed-file attribution with repository source." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Rewrite the reviewed Apache provenance checker as C# in the existing AGPL repository tool. Preserve the Apache source attribution and full licence; the derivative implementation is distributed under AGPL-3.0-only. Add no Python/Node runtime requirement and no sibling source build dependency.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": false + }, + "supersedes": null +} diff --git a/eng/provenance/records/contracts-provenance-csharp-r2.json b/eng/provenance/records/contracts-provenance-csharp-r2.json new file mode 100644 index 0000000..cb7c19f --- /dev/null +++ b/eng/provenance/records/contracts-provenance-csharp-r2.json @@ -0,0 +1,68 @@ +{ + "schemaVersion": 1, + "id": "contracts-provenance-csharp-r2", + "kind": "source", + "sourceRepository": "https://github.com/ArcForges/Contracts", + "sourceCommit": "18a970c67c463f1971ca05773b80f31a1b1ba1a7", + "sourcePaths": [ + "eng/check_provenance.py" + ], + "licence": { + "spdx": "Apache-2.0", + "category": "permissive", + "evidence": [ + { + "path": "eng/check_provenance.py", + "sha256": "c9e888d9845bd479c36439922c26038f29807214740ad7c467dfb3bb414a3024", + "finding": "The individual Python file declares SPDX-License-Identifier: Apache-2.0." + } + ], + "scope": "Rewrite the reviewed Apache provenance checker as C# in the existing AGPL repository tool. Preserve the Apache source attribution and full licence; the derivative implementation is distributed under AGPL-3.0-only. Add no Python/Node runtime requirement and no sibling source build dependency.", + "copyingPermission": null + }, + "attribution": [ + "Copyright ArcForges contributors. Original provenance checker: Apache-2.0; full text in eng/third-party/LICENSE.Contracts.txt.", + "ArcForges modifications: C# rewrite, existing repository-tool integration and portable archive notice validation." + ], + "targets": [ + { + "path": "eng/ArcForges.Repository/ProvenancePolicy.cs", + "sha256": "0da29d6f3cd9656640276d27eab25ee38fec68809d4dcc68ef3c1ad96ec1f3b9", + "normalization": "lf" + } + ], + "artifactTargets": [], + "disposition": "Rewrite", + "verification": { + "kind": "transformation", + "command": "dotnet run --project eng/ArcForges.Repository -- check", + "expected": "Independent C# positive/negative provenance tests, real Git inventory and history checks, deterministic NOTICE agreement with the Apache checker, and immutable reviewed target hash.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright ArcForges contributors. Original provenance checker: Apache-2.0; full text in eng/third-party/LICENSE.Contracts.txt.\nArcForges modifications: C# rewrite, existing repository-tool integration and portable archive notice validation.", + "files": [ + "eng/third-party/LICENSE.Contracts.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "source", + "reason": "This is source/build tooling; it is absent from the Native AOT application. Retain the original legal text and changed-file attribution with repository source." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Rewrite the reviewed Apache provenance checker as C# in the existing AGPL repository tool. Preserve the Apache source attribution and full licence; the derivative implementation is distributed under AGPL-3.0-only. Add no Python/Node runtime requirement and no sibling source build dependency. Correct the summary heading to match the independently implemented Apache renderer; keep revision 1 unchanged.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": false + }, + "supersedes": "contracts-provenance-csharp-r1" +} diff --git a/eng/provenance/records/upstream-avalonia-legal-r1.json b/eng/provenance/records/upstream-avalonia-legal-r1.json new file mode 100644 index 0000000..52199e8 --- /dev/null +++ b/eng/provenance/records/upstream-avalonia-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "upstream-avalonia-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/AvaloniaUI/Avalonia", + "sourceCommit": "d3c867a9e2de379249b03dbeb3495bd7f076a81a", + "sourcePaths": [ + "licence.md" + ], + "licence": { + "spdx": "MIT", + "category": "permissive", + "evidence": [ + { + "path": "licence.md", + "sha256": "213814d306090074d234d760239ff0f67eb9b8d20eefb4d5631bb39dbe0b769b", + "finding": "The retained file is the exact full governing legal text at the pinned source revision; byte comparison with the downloaded official source passed." + } + ], + "scope": "Retain the full legal document for Avalonia 12.1.2; no implementation is imported under this legal-text record.", + "copyingPermission": "Verbatim reproduction of the licence/copyright document to fulfil the selected MIT redistribution notice terms; the complete permission and disclaimer are preserved." + }, + "attribution": [ + "Copyright (c) AvaloniaUI OÜ. All Rights Reserved." + ], + "targets": [ + { + "path": "third-party/Avalonia.LICENSE.txt", + "sha256": "213814d306090074d234d760239ff0f67eb9b8d20eefb4d5631bb39dbe0b769b", + "normalization": "raw" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Compare third-party/Avalonia.LICENSE.txt with https://github.com/AvaloniaUI/Avalonia/blob/d3c867a9e2de379249b03dbeb3495bd7f076a81a/licence.md and the fixed SHA-256; run the C# repository check.", + "expected": "Both raw SHA-256 values equal 213814d306090074d234d760239ff0f67eb9b8d20eefb4d5631bb39dbe0b769b. The package retains the same full text under notices/upstream/.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright (c) AvaloniaUI OÜ. All Rights Reserved.\nRetain the complete licence, permissions and disclaimer in third-party/Avalonia.LICENSE.txt.", + "files": [ + "third-party/Avalonia.LICENSE.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reviewed exact official legal text and recorded raw-byte comparison for the existing Avalonia 12.1.2 dependency. This reconciles an existing retained document without claiming historical pre-copy approval or introducing its implementation.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/records/upstream-dotnet-legal-r1.json b/eng/provenance/records/upstream-dotnet-legal-r1.json new file mode 100644 index 0000000..14bbccb --- /dev/null +++ b/eng/provenance/records/upstream-dotnet-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "upstream-dotnet-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/dotnet/dotnet", + "sourceCommit": "95017c711e6afc1085133d440e42b4bd78155701", + "sourcePaths": [ + "LICENSE.TXT" + ], + "licence": { + "spdx": "MIT", + "category": "permissive", + "evidence": [ + { + "path": "LICENSE.TXT", + "sha256": "ae48df11a335dc1a615f4f938b69cba73bcf4485c4f97af49b38efb0f216353b", + "finding": "The retained file is the exact full governing legal text at the pinned source revision; byte comparison with the downloaded official source passed." + } + ], + "scope": "Retain the full legal document for .NET / Microsoft.Extensions 10.0.12; no implementation is imported under this legal-text record.", + "copyingPermission": "Verbatim reproduction of the licence/copyright document to fulfil the selected MIT redistribution notice terms; the complete permission and disclaimer are preserved." + }, + "attribution": [ + "Copyright (c) .NET Foundation and Contributors. All rights reserved." + ], + "targets": [ + { + "path": "third-party/dotnet.LICENSE.txt", + "sha256": "ae48df11a335dc1a615f4f938b69cba73bcf4485c4f97af49b38efb0f216353b", + "normalization": "raw" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Compare third-party/dotnet.LICENSE.txt with https://github.com/dotnet/dotnet/blob/95017c711e6afc1085133d440e42b4bd78155701/LICENSE.TXT and the fixed SHA-256; run the C# repository check.", + "expected": "Both raw SHA-256 values equal ae48df11a335dc1a615f4f938b69cba73bcf4485c4f97af49b38efb0f216353b. The package retains the same full text under notices/upstream/.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright (c) .NET Foundation and Contributors. All rights reserved.\nRetain the complete licence, permissions and disclaimer in third-party/dotnet.LICENSE.txt.", + "files": [ + "third-party/dotnet.LICENSE.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reviewed exact official legal text and recorded raw-byte comparison for the existing .NET / Microsoft.Extensions 10.0.12 dependency. This reconciles an existing retained document without claiming historical pre-copy approval or introducing its implementation.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/records/upstream-grpc-legal-r1.json b/eng/provenance/records/upstream-grpc-legal-r1.json new file mode 100644 index 0000000..cfb97bb --- /dev/null +++ b/eng/provenance/records/upstream-grpc-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "upstream-grpc-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/grpc/grpc-dotnet", + "sourceCommit": "4301104498e53898a452e8fb2fea6c0b1492b755", + "sourcePaths": [ + "LICENSE" + ], + "licence": { + "spdx": "Apache-2.0", + "category": "permissive", + "evidence": [ + { + "path": "LICENSE", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "finding": "The retained file is the exact full governing legal text at the pinned source revision; byte comparison with the downloaded official source passed." + } + ], + "scope": "Retain the full legal document for gRPC for .NET 2.83.0; no implementation is imported under this legal-text record.", + "copyingPermission": "Verbatim reproduction of the licence/copyright document to fulfil the selected Apache-2.0 redistribution notice terms; the complete permission and disclaimer are preserved." + }, + "attribution": [ + "Apache License, Version 2.0. gRPC for .NET contributors; full source licence is retained." + ], + "targets": [ + { + "path": "third-party/gRPC.LICENSE.txt", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "normalization": "raw" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Compare third-party/gRPC.LICENSE.txt with https://github.com/grpc/grpc-dotnet/blob/4301104498e53898a452e8fb2fea6c0b1492b755/LICENSE and the fixed SHA-256; run the C# repository check.", + "expected": "Both raw SHA-256 values equal cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30. The package retains the same full text under notices/upstream/.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Apache License, Version 2.0. gRPC for .NET contributors; full source licence is retained.\nRetain the complete licence, permissions and disclaimer in third-party/gRPC.LICENSE.txt.", + "files": [ + "third-party/gRPC.LICENSE.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reviewed exact official legal text and recorded raw-byte comparison for the existing gRPC for .NET 2.83.0 dependency. This reconciles an existing retained document without claiming historical pre-copy approval or introducing its implementation.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/records/upstream-microcom-legal-r1.json b/eng/provenance/records/upstream-microcom-legal-r1.json new file mode 100644 index 0000000..fe192f6 --- /dev/null +++ b/eng/provenance/records/upstream-microcom-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "upstream-microcom-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/kekekeks/MicroCom", + "sourceCommit": "76785efcafd91b5902fd19dd11145f6dd655b7b4", + "sourcePaths": [ + "LICENSE" + ], + "licence": { + "spdx": "MIT", + "category": "permissive", + "evidence": [ + { + "path": "LICENSE", + "sha256": "6ee769c9ac4dac9abb16b98b1341e9528ff9f4ab685481410d3376d14148f3a9", + "finding": "The retained file is the exact full governing legal text at the pinned source revision; byte comparison with the downloaded official source passed." + } + ], + "scope": "Retain the full legal document for MicroCom.Runtime 0.11.6; no implementation is imported under this legal-text record.", + "copyingPermission": "Verbatim reproduction of the licence/copyright document to fulfil the selected MIT redistribution notice terms; the complete permission and disclaimer are preserved." + }, + "attribution": [ + "Copyright (c) 2021 Nikita Tsukanov" + ], + "targets": [ + { + "path": "third-party/MicroCom.LICENSE.txt", + "sha256": "6ee769c9ac4dac9abb16b98b1341e9528ff9f4ab685481410d3376d14148f3a9", + "normalization": "raw" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Compare third-party/MicroCom.LICENSE.txt with https://github.com/kekekeks/MicroCom/blob/76785efcafd91b5902fd19dd11145f6dd655b7b4/LICENSE and the fixed SHA-256; run the C# repository check.", + "expected": "Both raw SHA-256 values equal 6ee769c9ac4dac9abb16b98b1341e9528ff9f4ab685481410d3376d14148f3a9. The package retains the same full text under notices/upstream/.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright (c) 2021 Nikita Tsukanov\nRetain the complete licence, permissions and disclaimer in third-party/MicroCom.LICENSE.txt.", + "files": [ + "third-party/MicroCom.LICENSE.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reviewed exact official legal text and recorded raw-byte comparison for the existing MicroCom.Runtime 0.11.6 dependency. This reconciles an existing retained document without claiming historical pre-copy approval or introducing its implementation.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/records/upstream-protobuf-legal-r1.json b/eng/provenance/records/upstream-protobuf-legal-r1.json new file mode 100644 index 0000000..434c1f5 --- /dev/null +++ b/eng/provenance/records/upstream-protobuf-legal-r1.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "id": "upstream-protobuf-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/protocolbuffers/protobuf", + "sourceCommit": "f377bfefc5e2cfab68b816903c25b23e091c439d", + "sourcePaths": [ + "LICENSE" + ], + "licence": { + "spdx": "BSD-3-Clause", + "category": "permissive", + "evidence": [ + { + "path": "LICENSE", + "sha256": "6e5e117324afd944dcf67f36cf329843bc1a92229a8cd9bb573d7a83130fea7d", + "finding": "The retained file is the exact full governing legal text at the pinned source revision; byte comparison with the downloaded official source passed." + } + ], + "scope": "Retain the full legal document for Google.Protobuf 3.36.1; no implementation is imported under this legal-text record.", + "copyingPermission": "Verbatim reproduction of the licence/copyright document to fulfil the selected BSD-3-Clause redistribution notice terms; the complete permission and disclaimer are preserved." + }, + "attribution": [ + "Copyright 2008 Google Inc. All rights reserved." + ], + "targets": [ + { + "path": "third-party/Protobuf.LICENSE.txt", + "sha256": "6e5e117324afd944dcf67f36cf329843bc1a92229a8cd9bb573d7a83130fea7d", + "normalization": "raw" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Compare third-party/Protobuf.LICENSE.txt with https://github.com/protocolbuffers/protobuf/blob/f377bfefc5e2cfab68b816903c25b23e091c439d/LICENSE and the fixed SHA-256; run the C# repository check.", + "expected": "Both raw SHA-256 values equal 6e5e117324afd944dcf67f36cf329843bc1a92229a8cd9bb573d7a83130fea7d. The package retains the same full text under notices/upstream/.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright 2008 Google Inc. All rights reserved.\nRetain the complete licence, permissions and disclaimer in third-party/Protobuf.LICENSE.txt.", + "files": [ + "third-party/Protobuf.LICENSE.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reviewed exact official legal text and recorded raw-byte comparison for the existing Google.Protobuf 3.36.1 dependency. This reconciles an existing retained document without claiming historical pre-copy approval or introducing its implementation.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/records/upstream-tmds-dbus-legal-r1.json b/eng/provenance/records/upstream-tmds-dbus-legal-r1.json new file mode 100644 index 0000000..2365b37 --- /dev/null +++ b/eng/provenance/records/upstream-tmds-dbus-legal-r1.json @@ -0,0 +1,69 @@ +{ + "schemaVersion": 1, + "id": "upstream-tmds-dbus-legal-r1", + "kind": "legal-text", + "sourceRepository": "https://github.com/tmds/Tmds.DBus", + "sourceCommit": "b4a7fed0b878f74cb54f7cca84d2889af4e596ba", + "sourcePaths": [ + "COPYING" + ], + "licence": { + "spdx": "MIT", + "category": "permissive", + "evidence": [ + { + "path": "COPYING", + "sha256": "aca3b49277abcc7886ccb2f115603a9f5b15fc3f96019a10c00f2974098b210c", + "finding": "The retained file is the exact full governing legal text at the pinned source revision; byte comparison with the downloaded official source passed." + } + ], + "scope": "Retain the full legal document for Tmds.DBus.Protocol 0.94.1; no implementation is imported under this legal-text record.", + "copyingPermission": "Verbatim reproduction of the licence/copyright document to fulfil the selected MIT redistribution notice terms; the complete permission and disclaimer are preserved." + }, + "attribution": [ + "Copyright 2006 Alp Toker ", + "Copyright 2010 Other Contributors", + "Copyright 2016 Tom Deseyn " + ], + "targets": [ + { + "path": "third-party/Tmds.DBus.LICENSE.txt", + "sha256": "aca3b49277abcc7886ccb2f115603a9f5b15fc3f96019a10c00f2974098b210c", + "normalization": "raw" + } + ], + "artifactTargets": [], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Compare third-party/Tmds.DBus.LICENSE.txt with https://github.com/tmds/Tmds.DBus/blob/b4a7fed0b878f74cb54f7cca84d2889af4e596ba/COPYING and the fixed SHA-256; run the C# repository check.", + "expected": "Both raw SHA-256 values equal aca3b49277abcc7886ccb2f115603a9f5b15fc3f96019a10c00f2974098b210c. The package retains the same full text under notices/upstream/.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "Copyright 2006 Alp Toker \nCopyright 2010 Other Contributors\nCopyright 2016 Tom Deseyn \nRetain the complete licence, permissions and disclaimer in third-party/Tmds.DBus.LICENSE.txt.", + "files": [ + "third-party/Tmds.DBus.LICENSE.txt", + "eng/provenance/NOTICE.txt" + ], + "distribution": "packages", + "reason": "The original full root legal document and package-specific source summary accompany each portable archive." + }, + "lifetime": { + "status": "permanent", + "owner": "ArcNotes Licensing and Provenance Owner", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "approved", + "rationale": "Reviewed exact official legal text and recorded raw-byte comparison for the existing Tmds.DBus.Protocol 0.94.1 dependency. This reconciles an existing retained document without claiming historical pre-copy approval or introducing its implementation.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": true + }, + "supersedes": null +} diff --git a/eng/provenance/template.json b/eng/provenance/template.json new file mode 100644 index 0000000..84976a5 --- /dev/null +++ b/eng/provenance/template.json @@ -0,0 +1,70 @@ +{ + "schemaVersion": 1, + "instructions": "Copy this shape into records/.json and complete every field before reuse. This template is deliberately not an approvable record. Generated material requires nonempty generators and inputs with repository, commit, paths, spdx and file-level evidence; temporary material requires a removalTrigger. Used records are immutable; create a superseding revision. See docs/provenance.md.", + "example": { + "schemaVersion": 1, + "id": "material-name-r1", + "kind": "source", + "sourceRepository": "https://github.com/OWNER/REPOSITORY", + "sourceCommit": "FULL_IMMUTABLE_COMMIT", + "sourcePaths": [ + "EXACT/SOURCE/PATH" + ], + "licence": { + "spdx": "REVIEWED_SPDX_EXPRESSION", + "category": "REVIEWED_POLICY_CATEGORY", + "evidence": [ + { + "path": "GOVERNING/LICENCE/OR/FILE", + "sha256": "SHA256_OF_LICENCE_EVIDENCE", + "finding": "Explain the file-level licence and any subtree override." + } + ], + "scope": "Describe precisely which material and rights are used.", + "copyingPermission": null + }, + "attribution": [ + "EXACT_COPYRIGHT_AND_ATTRIBUTION" + ], + "targets": [ + { + "path": "EXACT/TARGET/PATH", + "sha256": "SHA256_OF_REVIEWED_TARGET", + "normalization": "lf" + } + ], + "disposition": "Copy", + "verification": { + "kind": "byte-match", + "command": "Reproducible comparison or generation command.", + "expected": "Independent expected result.", + "artifacts": [] + }, + "notice": { + "required": true, + "text": "EXACT_REQUIRED_NOTICE", + "files": [ + "EXACT/NOTICE/PATH" + ], + "distribution": "source", + "reason": "Explain whether material is distributed in packages and where its notice is retained." + }, + "lifetime": { + "status": "permanent", + "owner": "ACCOUNTABLE_OWNER", + "removalTrigger": null + }, + "generation": null, + "review": { + "owner": "Licensing and Provenance Owner", + "reviewer": "Codex, acting under the repository maintainer's implementation and review authorization", + "reviewedOn": "2026-09-19", + "decision": "PENDING_REVIEW", + "rationale": "Describe the file-level compatibility finding before introducing material.", + "baselineCommit": "ce797cd18d368b57dc4c9bc29112a9467172ac6d", + "reconciliation": false + }, + "supersedes": null, + "artifactTargets": [] + } +} diff --git a/eng/third-party/LICENSE.Contracts.txt b/eng/third-party/LICENSE.Contracts.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/eng/third-party/LICENSE.Contracts.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs b/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs index e28886f..9dcbae8 100644 --- a/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs +++ b/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs @@ -1,5 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +using System.Formats.Tar; +using System.IO.Compression; using System.Security.Cryptography; +using System.Text; using System.Text.Json; using ArcForges.Repository; using Xunit; @@ -20,10 +23,10 @@ public void CiAttemptsHaveDifferentImmutableVersions() public void ReleaseRejectsTamperingOrWrongSource() { using var fixture = new CandidateFixture(); - Assert.Equal("win-x64", Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit)); - Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, new string('b', 40))); + Assert.Equal("win-x64", Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); + Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, new string('b', 40), fixture.SourceRoot)); File.AppendAllText(fixture.Archive, "changed after validation"); - Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit)); + Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); } [Theory] @@ -32,7 +35,37 @@ public void ReleaseRejectsTamperingOrWrongSource() public void ReleaseRejectsPathEscapeOrFailedLiveEvidence(string? unsafeArchive, bool success) { using var fixture = new CandidateFixture(unsafeArchive, success); - Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit)); + Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); + } + + [Theory] + [InlineData("win-x64")] + [InlineData("linux-x64")] + public void ZipAndTarNoticesAreActuallyRead(string rid) + { + using var fixture = new CandidateFixture(rid: rid); + Assert.Equal(rid, Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); + } + + [Theory] + [InlineData("win-x64", "missing")] + [InlineData("win-x64", "changed")] + [InlineData("win-x64", "duplicate")] + [InlineData("win-x64", "wrong-source")] + [InlineData("win-x64", "dirty")] + [InlineData("win-x64", "case-collision")] + [InlineData("win-x64", "escape")] + [InlineData("linux-x64", "missing")] + [InlineData("linux-x64", "changed")] + [InlineData("linux-x64", "duplicate")] + [InlineData("linux-x64", "wrong-source")] + [InlineData("linux-x64", "dirty")] + [InlineData("linux-x64", "case-collision")] + [InlineData("linux-x64", "escape")] + public void MatchingOuterHashesCannotHideMissingLegalTextOrWrongSource(string rid, string mode) + { + using var fixture = new CandidateFixture(rid: rid, mode: mode); + Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); } private sealed class CandidateFixture : IDisposable @@ -40,19 +73,50 @@ private sealed class CandidateFixture : IDisposable private readonly string _folder = Path.Combine(Path.GetTempPath(), "arcnotes-test-" + Guid.NewGuid()); public string Version => "0.1.0-ci.23.1"; public string Commit => new('a', 40); + public string SourceRoot { get; } public string Manifest => Path.Combine(_folder, "manifest.json"); - public string Archive => Path.Combine(_folder, $"arcnotes-{Version}-win-x64.zip"); + public string Archive { get; } - public CandidateFixture(string? unsafeArchive = null, bool success = true) + public CandidateFixture(string? unsafeArchive = null, bool success = true, string rid = "win-x64", string mode = "valid") { + var repo = new DirectoryInfo(AppContext.BaseDirectory); + while (!File.Exists(Path.Combine(repo.FullName, ProvenancePolicy.Policy))) repo = repo.Parent ?? throw new InvalidOperationException("Repository policy not found."); + SourceRoot = repo.FullName; + Archive = Path.Combine(_folder, $"arcnotes-{Version}-{rid}" + (rid.StartsWith("win-", StringComparison.Ordinal) ? ".zip" : ".tar.gz")); Directory.CreateDirectory(_folder); - File.WriteAllText(Archive, "synthetic archive bytes for hash validation"); + var entries = ProvenancePolicy.PackageNotices(SourceRoot).Select(n => (n.Key, n.Value)).ToList(); + entries.Add(("notices/provenance-source.json", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { result = "passed", sourceCommit = mode == "wrong-source" ? new string('b', 40) : Commit, dirty = mode == "dirty" })))); + if (mode == "missing") entries.RemoveAll(e => e.Key == "LICENSE"); + if (mode == "changed") entries[entries.FindIndex(e => e.Key == "LICENSE")] = ("LICENSE", Encoding.UTF8.GetBytes("abbreviated licence\n")); + if (mode == "duplicate") entries.Add(entries.Single(e => e.Key == "LICENSE")); + if (mode == "case-collision") entries.Add(("license", Encoding.UTF8.GetBytes("shadowed legal text"))); + if (mode == "escape") entries.Add(("../outside", Encoding.UTF8.GetBytes("outside member"))); + if (Archive.EndsWith(".zip", StringComparison.Ordinal)) + { + using var zip = ZipFile.Open(Archive, ZipArchiveMode.Create); + foreach (var notice in entries) + { + using var output = zip.CreateEntry(notice.Key).Open(); + output.Write(notice.Value); + } + } + else + { + using var output = File.Create(Archive); + using var gzip = new GZipStream(output, CompressionLevel.Optimal); + using var tar = new TarWriter(gzip); + foreach (var notice in entries) + { + using var data = new MemoryStream(notice.Value); + tar.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, notice.Key) { DataStream = data }); + } + } var smoke = Path.Combine(_folder, "smoke.json"); File.WriteAllText(smoke, JsonSerializer.Serialize(new { success, nativeAot = true, - rid = "win-x64", + rid, sourceRevision = Commit, version = Version, uiGreeting = "Hello, ArcNotes!", @@ -61,7 +125,7 @@ public CandidateFixture(string? unsafeArchive = null, bool success = true) })); var hash = Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(Archive))); File.WriteAllText(Archive + ".sha256", $"{hash} {Path.GetFileName(Archive)}\n"); - File.WriteAllText(Manifest, JsonSerializer.Serialize(new Candidate("win-x64", Version, Commit, + File.WriteAllText(Manifest, JsonSerializer.Serialize(new Candidate(rid, Version, Commit, unsafeArchive ?? Path.GetFileName(Archive), hash, Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(smoke)))))); } diff --git a/tests/ArcForges.ArcNotes.Tests/ProvenancePolicyTests.cs b/tests/ArcForges.ArcNotes.Tests/ProvenancePolicyTests.cs new file mode 100644 index 0000000..ca3f30d --- /dev/null +++ b/tests/ArcForges.ArcNotes.Tests/ProvenancePolicyTests.cs @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using ArcForges.Repository; +using Xunit; + +namespace ArcForges.ArcNotes.Tests; + +public sealed class ProvenancePolicyTests +{ + [Fact] + public void CompleteRecordAndInventoryPass() + { + using var fixture = new Fixture(); + var result = fixture.Validate(); + Assert.Equal("passed", result.Result); + Assert.Equal(1, result.ReusedFiles); + Assert.Equal(["fixture-source-r1"], result.ActiveRecords); + Assert.StartsWith("ArcForges source provenance notices\n", fixture.Read(ProvenancePolicy.Summary), StringComparison.Ordinal); + } + + [Theory] + [InlineData("sourceRepository")] + [InlineData("sourceCommit")] + [InlineData("sourcePaths")] + [InlineData("licence")] + [InlineData("attribution")] + [InlineData("targets")] + [InlineData("disposition")] + [InlineData("verification")] + [InlineData("notice")] + [InlineData("lifetime")] + public void EveryRequiredFieldIsChecked(string field) + { + using var fixture = new Fixture(); + fixture.Record.Remove(field); + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + } + + [Theory] + [InlineData("sourceRepository", " ")] + [InlineData("sourceRepository", "http://example.org/reference")] + [InlineData("sourceRepository", "https://example.org/reference/../other")] + [InlineData("sourceCommit", "moving-tag")] + [InlineData("disposition", "Unreviewed")] + [InlineData("kind", "unknown")] + public void InvalidIdentityAndDispositionFail(string field, string value) + { + using var fixture = new Fixture(); + fixture.Record[field] = value; + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + } + + [Theory] + [InlineData("GPL-3.0-only", "gpl-only")] + [InlineData("EPL-1.0", "incompatible")] + [InlineData("NOASSERTION", "unclear")] + [InlineData("MIT", "agpl-compatible")] + public void ProhibitedOrMismatchedLicenceFails(string expression, string category) + { + using var fixture = new Fixture(); + fixture.Record["licence"]!["spdx"] = expression; + fixture.Record["licence"]!["category"] = category; + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + } + + [Fact] + public void EditingDecisionDataCannotGrantPermission() + { + using var fixture = new Fixture(); + var policy = JsonNode.Parse(fixture.Read(ProvenancePolicy.Policy))!; + policy["decisions"]!["gpl-only"]!["AGPL"] = "audit"; + fixture.Write(ProvenancePolicy.Policy, policy.ToJsonString()); + Assert.Throws(() => fixture.Validate()); + } + + [Theory] + [InlineData("../outside.cs")] + [InlineData("/absolute.cs")] + [InlineData("C:/outside.cs")] + [InlineData("src/*.cs")] + [InlineData("src/../reused.cs")] + public void RecordPathsCannotEscapeOrUsePatterns(string path) + { + using var fixture = new Fixture(); + fixture.Record["targets"]![0]!["path"] = path; + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + } + + [Fact] + public void UnknownFileAndChangedOrMissingTargetFail() + { + using var fixture = new Fixture(); + fixture.Write("unclassified.cs", "new file\n"); + Assert.Throws(() => fixture.Validate()); + fixture.Sync(); + fixture.Write("src/reused.cs", "changed reused file\n"); + Assert.Throws(() => fixture.Validate()); + File.Delete(Path.Combine(fixture.Root, "src/reused.cs")); + Assert.Throws(() => fixture.Validate()); + } + + [Fact] + public void MissingRecordAndChangedNoticeFail() + { + using var fixture = new Fixture(); + fixture.Reused["src/reused.cs"] = "missing-record-r1"; + fixture.Sync(); + Assert.Throws(() => fixture.Validate()); + fixture.Reused["src/reused.cs"] = "fixture-source-r1"; + fixture.Sync(); + fixture.Write(ProvenancePolicy.Summary, "not the generated summary\n"); + Assert.Throws(() => fixture.Validate()); + } + + [Fact] + public void TemporaryAndGeneratedMaterialRequireConditionalEvidence() + { + using var fixture = new Fixture(); + fixture.Record["lifetime"]!["status"] = "temporary"; + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + fixture.Record["lifetime"]!["removalTrigger"] = "Remove when the assigned native owner replaces the fixture."; + fixture.SaveRecord(); + fixture.Validate(writeNotice: true); + fixture.Record["kind"] = "generated"; + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + fixture.Record["generation"] = JsonNode.Parse("""{"generators":[],"inputs":[],"command":"fixture generator","outputSpdx":"MIT"}"""); + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + } + + [Fact] + public void LegalTextPermissionCannotBeUsedForSourceCode() + { + using var fixture = new Fixture(); + fixture.Record["kind"] = "legal-text"; + fixture.Record["licence"]!["copyingPermission"] = "Permission to copy the legal document."; + fixture.SaveRecord(); + Assert.Throws(() => fixture.Validate()); + } + + [Fact] + public void UsedRecordsCannotBeChangedOrRemoved() + { + using var fixture = new Fixture(); + var history = fixture.History(); + fixture.Record["attribution"]![0] = "changed attribution"; + fixture.SaveRecord(); + Assert.Contains("Used record", Assert.Throws(() => fixture.Validate(history)).Message, StringComparison.Ordinal); + File.Delete(Path.Combine(fixture.Root, Fixture.RecordPath)); + fixture.Sync(); + Assert.Contains("Used record", Assert.Throws(() => fixture.Validate(history)).Message, StringComparison.Ordinal); + } + + [Fact] + public void RetainedReuseCannotBecomeFirstParty() + { + using var fixture = new Fixture(); + using var previous = JsonDocument.Parse(fixture.Read(ProvenancePolicy.Inventory)); + var history = fixture.History(); + fixture.Reused.Clear(); + var second = (JsonObject)fixture.Record.DeepClone(); + second["id"] = "second-source-r1"; + second["targets"]![0]!["path"] = "second.cs"; + fixture.Write("second.cs", Fixture.Source); + fixture.Write("eng/provenance/records/second-source-r1.json", second.ToJsonString()); + fixture.Reused.Add("second.cs", "second-source-r1"); + fixture.Sync(); + Assert.Contains("lost provenance", Assert.Throws(() => fixture.Validate(history, previous.RootElement)).Message, StringComparison.Ordinal); + } + + [Fact] + public void SupersedingRevisionPreservesTheOriginalAndItsBinding() + { + using var fixture = new Fixture(); + using var previous = JsonDocument.Parse(fixture.Read(ProvenancePolicy.Inventory)); + var history = fixture.History(); + var revised = (JsonObject)fixture.Record.DeepClone(); + revised["id"] = "fixture-source-r2"; + revised["supersedes"] = "fixture-source-r1"; + fixture.Write("eng/provenance/records/fixture-source-r2.json", revised.ToJsonString()); + fixture.Reused["src/reused.cs"] = "fixture-source-r2"; + fixture.Sync(); + Assert.Equal(2, fixture.Validate(history, previous.RootElement, true).Records); + revised["supersedes"] = "unknown-r1"; + fixture.Write("eng/provenance/records/fixture-source-r2.json", revised.ToJsonString()); + Assert.Throws(() => fixture.Validate(history, previous.RootElement)); + } + + [Fact] + public void DuplicateJsonFieldsAreRejected() + { + using var fixture = new Fixture(); + fixture.Write(Fixture.RecordPath, fixture.Read(Fixture.RecordPath).Replace("\"schemaVersion\":1", "\"schemaVersion\":1,\"schemaVersion\":1", StringComparison.Ordinal)); + Assert.Contains("Duplicate JSON", Assert.Throws(() => fixture.Validate()).Message, StringComparison.Ordinal); + } + + [Fact] + public void LinkedDirectoryCannotReadOutsideTheOwner() + { + using var fixture = new Fixture(); + var link = Path.Combine(fixture.Root, "linked"); + var target = Path.Combine(fixture.Root, "src"); + if (OperatingSystem.IsWindows()) + { + var info = new ProcessStartInfo("cmd.exe") { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; + foreach (var argument in new[] { "/c", "mklink", "/J", link, target }) info.ArgumentList.Add(argument); + using var process = Process.Start(info)!; + Assert.True(process.WaitForExit(10000)); + Assert.Equal(0, process.ExitCode); + } + else Directory.CreateSymbolicLink(link, target); + Assert.Contains("Linked", Assert.Throws(() => ProvenancePolicy.Read(fixture.Root, "linked/reused.cs")).Message, StringComparison.Ordinal); + Directory.Delete(link); + } + + [Fact] + public void EventBaseComesFromTheTrustedEvent() + { + using var pr = JsonDocument.Parse("""{"pull_request":{"base":{"sha":"base-identity"},"head":{"sha":"untrusted-head"}}}"""); + Assert.Equal("base-identity", ProvenancePolicy.EventBase("pull_request", pr.RootElement)); + using var push = JsonDocument.Parse("""{"before":"previous-main","after":"new-main"}"""); + Assert.Equal("previous-main", ProvenancePolicy.EventBase("push", push.RootElement)); + } + + [Fact] + public void PackageNoticePresenceAndFullBytesAreRequired() + { + var expected = new Dictionary { ["notices/upstream/LICENSE.txt"] = Encoding.UTF8.GetBytes("full permission and disclaimer\n") }; + ProvenancePolicy.VerifyPackageNotices(expected, path => expected[path]); + Assert.Throws(() => ProvenancePolicy.VerifyPackageNotices(expected, _ => null)); + Assert.Throws(() => ProvenancePolicy.VerifyPackageNotices(expected, _ => Encoding.UTF8.GetBytes("abbreviated permission\n"))); + } + + private sealed class Fixture : IDisposable + { + public const string Source = "// Synthetic first-party test fixture, not real reused material.\n"; + public const string RecordPath = "eng/provenance/records/fixture-source-r1.json"; + public string Root { get; } = Path.Combine(Path.GetTempPath(), "arcnotes-provenance-test-" + Guid.NewGuid()); + public Dictionary Reused { get; } = new(StringComparer.Ordinal) { ["src/reused.cs"] = "fixture-source-r1" }; + public JsonObject Record { get; } + + public Fixture() + { + var repo = new DirectoryInfo(AppContext.BaseDirectory); + while (!File.Exists(Path.Combine(repo.FullName, ProvenancePolicy.Policy))) repo = repo.Parent ?? throw new InvalidOperationException("Repository fixture policy was not found."); + Write(ProvenancePolicy.Policy, File.ReadAllText(Path.Combine(repo.FullName, ProvenancePolicy.Policy))); + Write("eng/provenance/template.json", File.ReadAllText(Path.Combine(repo.FullName, "eng/provenance/template.json"))); + Record = JsonSerializer.SerializeToNode(new + { + schemaVersion = 1, + id = "fixture-source-r1", + kind = "source", + sourceRepository = "https://example.org/reference", + sourceCommit = new string('a', 40), + sourcePaths = new[] { "source.cs" }, + licence = new { spdx = "MIT", category = "permissive", evidence = new[] { new { path = "LICENSE", sha256 = new string('b', 64), finding = "Synthetic file-level evidence for validator tests only." } }, scope = "Synthetic test fixture.", copyingPermission = (string?)null }, + attribution = new[] { "Synthetic fixture attribution." }, + targets = new[] { new { path = "src/reused.cs", sha256 = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(Source))), normalization = "lf" } }, + artifactTargets = System.Array.Empty(), + disposition = "Copy", + verification = new { kind = "byte-match", command = "fixture comparison", expected = "fixed fixture bytes", artifacts = System.Array.Empty() }, + notice = new { required = true, text = "Synthetic fixture notice.", files = new[] { "NOTICE.txt" }, distribution = "source", reason = "Test scope only." }, + lifetime = new { status = "permanent", owner = "Fixture owner", removalTrigger = (string?)null }, + generation = (object?)null, + review = new { owner = "Licensing and Provenance Owner", reviewer = "Synthetic test reviewer", reviewedOn = "2026-09-19", decision = "approved", rationale = "Test data; no actual reuse approval.", baselineCommit = new string('c', 40), reconciliation = false }, + supersedes = (string?)null + })!.AsObject(); + Write("src/reused.cs", Source); + Write("NOTICE.txt", "Synthetic fixture notice.\n"); + Write(ProvenancePolicy.Summary, ""); + Write(ProvenancePolicy.Inventory, "{}"); + SaveRecord(); + Sync(); + Validate(writeNotice: true); + } + + public string Read(string path) => File.ReadAllText(Path.Combine(Root, path)); + public void Write(string path, string text) + { + var full = Path.Combine(Root, path); + Directory.CreateDirectory(Path.GetDirectoryName(full)!); + File.WriteAllText(full, text); + } + public void SaveRecord() => Write(RecordPath, Record.ToJsonString()); + private string[] Files() => Directory.GetFiles(Root, "*", SearchOption.AllDirectories).Select(path => Path.GetRelativePath(Root, path).Replace('\\', '/')).ToArray(); + public void Sync() => Write(ProvenancePolicy.Inventory, JsonSerializer.Serialize(new { schemaVersion = 1, repository = "ArcNotes", firstParty = Files().Where(path => !Reused.ContainsKey(path)).ToArray(), reused = Reused, artifacts = System.Array.Empty() })); + public Dictionary History() => new(StringComparer.Ordinal) { [RecordPath] = File.ReadAllBytes(Path.Combine(Root, RecordPath)) }; + public ProvenanceResult Validate(IReadOnlyDictionary? history = null, JsonElement? prior = null, bool writeNotice = false) => + ProvenancePolicy.Validate(Root, Files(), history ?? new Dictionary(), prior, writeNotice); + public void Dispose() + { + var full = Path.GetFullPath(Root); + if (Path.GetDirectoryName(full) != Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.GetTempPath())) || !Path.GetFileName(full).StartsWith("arcnotes-provenance-test-", StringComparison.Ordinal)) + throw new InvalidOperationException("Unexpected fixture cleanup path."); + Directory.Delete(full, true); + } + } +}