Public report — Mirror, published 10 Sep 2026.
Concrete security findings (CVE IDs, secret matches, dependency versions) are hidden in this version;
ask the repo owner for the full report.
Public
Codebase surveyMeasured under the Code Assurance Index, but not filed — nothing here carries a record a reader can check independently. How a survey is verifiedNo record
Medium · 52,917 LoC · 1 projects · rebuild ~0.8 person-years · weakest lens: Code Health (30%)
Findings by grade
50 critical16 serious28 minor32 could not be resolved — could be critical — see Limitations
This survey was produced by
Watchdog
Producer
Canine Development
Analyzer
Watchdog engine 1.0.0
Measured
10 September 2026, 23:08 UTC
A measurement, not a certificate. The Code Assurance Index does not certify,
approve or guarantee this codebase; it records a reproducible number and the evidence it was computed from. The
standard is authored by Canine Development, who also build Watchdog — its only implementation today. That is said
here so the number is checked rather than believed.
Degraded — solution could not be loaded
The C# solution could not be loaded in the analyzer (the workspace returned 0 projects), so every compiler-dependent dimension ran on nothing and the size/effort figures were estimated directly from source text. This run is Degraded — treat the grade as indicative only. See diagnostics.md for the exact cause (which solution project references resolved vs were missing, a structure map of the analyzed tree, and the solution/project files), then re-run for a reliable result.
Grounded in facts. Every number here is computed, not narrated — reproducible, tool-backed, and traceable to a line of code. How to trust this ▸
76findings with an exact file:lineof 94 — the remainder are repo-wide signals (a dimension-level measurement, not a single line); open any file:line and verify
30/115dimensions across the health lenses52917 LoC · 1 projects — wide & deep
The system holds a provisional health score of 41%, indicating a workable asset carrying significant operational risk. While the codebase is medium-sized with substantial test coverage, its overall stability is compromised by poor code quality. This standing means that while the business logic is intact, the cost of maintaining and evolving the system is artificially high, threatening delivery speed and long-term reliability.
The primary value at stake lies in the core logic, which represents a rebuild effort of approximately 0.8 person-years, costing roughly €120,000. With nearly 40% of the code being branching logic, the system is complex enough that small changes can have large ripple effects. The high proportion of boilerplate suggests significant technical debt, meaning engineers spend more time navigating existing structures than building new features. This inefficiency directly impacts the cost of change and slows down time-to-market for new capabilities.
The most critical theme is fragile code health. With a score of only 30%, the codebase is difficult to maintain and prone to defects. This weakness is the biggest risk to the business, as it increases the likelihood of bugs in production and makes onboarding new team members slower and more error-prone. The lack of measured test reliability further amplifies this risk, as there is no safety net to catch regressions during updates. This creates a cycle where fear of breaking things leads to slower, more cautious development.
A secondary concern is security and operational readiness. The security score of 48% and readiness score of 52% indicate gaps in observability and secure practices. A leaked secret was identified, which poses an immediate exposure risk if not contained. Additionally, the absence of measured architecture and domain modeling signals means we cannot fully assess how well the system aligns with business goals or how resilient it is to structural changes. This uncertainty makes strategic planning difficult.
Despite these risks, the system demonstrates genuine strength in maturity, scoring 66%. This suggests that the core processes and documentation are reasonably established, providing a foundation for improvement. The substantial test codebase also indicates a historical commitment to quality, even if current execution is lacking.
Focus first on resolving the six write-only private field issues in the explicit debt areas. This action offers the highest leverage, requiring minimal effort but significantly improving code clarity and reducing the cognitive load on developers. Addressing these specific code smells will immediately enhance maintainability and reduce the risk of future defects, providing a quick win that stabilizes the foundation for broader improvements.
How the score is built — each lens's share of the headlineWidth is the lens's weight in the worst-heaviest fold (the weakest area pulls hardest); colour is that lens's own band. A lens fixes the score in proportion to its width.
This codebase represents roughly ~0.8 person-years of build effort (about ~€120,000 to rebuild). Its weakest lens is Code Health at 30% — the part of that asset most exposed by the findings below.
How we model this: boilerplate at a scaffolding rate + logic × domain Standard (×1.2) — desktop/game, high decision density × a 0.7× quality factor, at €60–95/h; indicative, ±~30% · size measured directly from source (the solution did not build in-analyzer). Indicative only — most sensitive to the hourly rate and the domain tier (both tunable in config).
Top priorities
The highest-leverage moves; the full ranked list is in the Roadmap below.
1
Resolve the 6 WriteOnlyPrivateField finding(s) in Explicit Debt — start with NetworkBehaviourClientRpcParamAbstract.cs, NetworkBehaviourClientRpcParamComponent.cs, NetworkBehaviourCmdParamAbstract.cs.
Value concentrated against a weak lens · High · Value at risk
This is a Medium asset (~0.8 person-years to rebuild), and its weakest lens is Code Health at 30%. The operational and business risk on an asset this size concentrates there — that's where remediation buys the most protection.
→ Direct remediation budget at Code Health first — highest risk-reduction per euro on an asset this size.
Highest-leverage move · Medium · Leverage
Of everything flagged, the best return on effort is: Resolve the 6 WriteOnlyPrivateField finding(s) in Explicit Debt — start with NetworkBehaviourClientRpcParamAbstract.cs, NetworkBehaviourClientRpcParamComponent.cs, NetworkBehaviourCmdParamAbstract.cs. The rest can wait behind it.
Evidence: priority ranking: top of 5 ranked by impact/effort
→ Resolve the 6 WriteOnlyPrivateField finding(s) in Explicit Debt — start with NetworkBehaviourClientRpcParamAbstract.cs, NetworkBehaviourClientRpcParamComponent.cs, NetworkBehaviourCmdParamAbstract.cs.
At a glance — Code Health · 30% · Weak · gated by D17 ·
Findings mapped to OWASP categories; the specific CVEs/secrets are in the Security dimension cards below and findings.md (redacted only on the public version of this report).
OWASP category
Findings
Severity
A03:2021 — Injection
26
High / Critical
A02:2021 — Cryptographic Failures
6
High / Critical
A05:2021 — Security Misconfiguration
5
High / Critical
A06:2021 — Vulnerable & Outdated Components
1
Medium
Roadmap
Begin by addressing explicit code debt, specifically resolving write-only private fields in the NetworkBehaviour classes and removing unnecessary suppressions in the test project. Immediately remediate the leaked secret in the application version file to secure sensitive credentials. Finally, strengthen operational resilience by implementing draft release gates to prevent bad builds from reaching users and codifying geo-redundant backups in infrastructure as code to support disaster recovery.
Ranked by impact ÷ effort. "Helps" is the estimated gain on the 0–100 health score.
Do this
Helps
Effort
Dimension
Resolve the 6 WriteOnlyPrivateField finding(s) in Explicit Debt — start with NetworkBehaviourClientRpcParamAbstract.cs, NetworkBehaviourClientRpcParamComponent.cs, NetworkBehaviourCmdParamAbstract.cs.
Nothing pauses a release for a human: publish as a draft release (or gate the release job on a protected tag/manual dispatch) so a bad build can be stopped before users can download it.
Resolve the 15 High finding(s) charged to Static Analysis (SAST) — the other 10 are reported here at file:line but scored by D36 (supply-chain provenance), which charges them once.
Record significant decisions one document per decision — dated, stating the context, the decision and its consequences — and keep them together wherever your design docs already live (a conventional `docs/adr/` tree, with each file named `NNNN-title` in whatever markup those docs already use, is the most discoverable form).
Every finding carries one of four grades. Three say how serious it is. The fourth says this
survey could not settle it — and it is a grade, not a gap.
Critical — 50
A definite problem that already costs you something and drags the score down: a
missing authorisation check, a dependency with a known exploit, a build that does not reproduce. Failure here
tends to cause failures elsewhere.
Serious — 16
Likely wrong, but not failing yet. It degrades
the codebase over a longer horizon and can cause failures elsewhere — not urgent this week, not something to
carry for two years either.
Minor — 28
Recorded, with no effect on how the codebase functions.
Present so the survey is complete, not because it needs doing.
Could not be resolved — 32
Something this survey could not settle
from the outside, and which could be critical or serious. Either a control was required and no
positive evidence of it exists in the repository — a backup job that nothing shows was ever restored from proves
nothing about restores — or our own analysis could not run over that part of the tree. This is not a clean
result. These are excluded from the score rather than awarded a pass, so the number on the cover neither
rewards nor penalises them: if you act on this survey without resolving them, you carry that risk yourself. Each
one is named under Limitations.
Methodology & how to trust this report
Watchdog is a deep, periodic assessment — run each sprint, monthly, or quarterly, taking the time to go wider and deeper than a quick check and surfacing in one coherent report what you'd otherwise piece together from a dozen separate tools. It scores deterministically: the same commit yields the same score, every run. 26 of 30 evaluated dimensions are computed purely by tools and static analysis (confidence 1.0); 4 documentation/naming judgement(s) are LLM-assisted and labelled advisory. Overall confidence is 0.5 — the weighted average across measured dimensions; it falls as more of the score leans on LLM-assisted judgement and rises when it's fully tool-backed.
Every figure here is one of three kinds, and we label which: ✓ Measured — a deterministic fact (LoC, complexity, coverage); ~ Modeled — an estimate from a stated model (cost, effort, value-at-risk), always a range with its assumptions, never a precise fact; ◐ Advisory — an LLM prose judgement. We never present a modelled estimate as if it were measured. Perfect or absent scores carry their provenance too (ADR-0011): ✓ Tool-verified means the property itself was measured across the surface; ○ Nothing flagged means the probes came back clean — a claim bounded by what a repository can show; ⊘ Not evidenced means a working control (a tested restore, an automated rollback) showed no positive evidence — absence of evidence is not evidence of a control, so it's excluded from the score rather than awarded a spurious 10; ◐ Sampled · advisory marks an LLM verdict over a bounded sample — advisory, never a deterministic measurement.
What we checked — 30 dimensions across the health lenses
Each chip is a dimension scored from real signals across architecture, testing, dependencies, security & compliance, documentation, git-history and code quality — in one coherent pass. A surface report typically covers a handful.
How to trust any code-health report — three questions
Can you open the finding? Real findings cite a repo-relative file and line you can open at the cited line — never an absolute scratch path. Here, 76 of 94 do; the remainder are repo-wide signals — a dimension-level measurement, not a single line. (Every path in this report is repo-relative by construction: paths are normalized at the producer and the report is rejected if any rooted path leaks through.)
Is there a tool behind the number? Every score below names the method that produced it — Roslyn, git, a scanner, or (for a handful of documentation/naming dimensions) an LLM labelled sampled · advisory — not a narrative.
Does re-running give the same result? Run it again on the same commit and the score — and this report, byte for byte — is identical. A report whose numbers move between runs is describing the run, not the code.
This report answers yes to all three. That's the bar to hold any assessment to.
Tools & methods
The actual versions used this run (captured at analysis time) — re-run on the same commit for the identical score.
Method
Backs
Version
Evaluator
Roslyn static analysis
Complexity, cohesion, coupling, dead code, API surface, layering
What ran differently this time — a tool absent, degraded, or that fell back to an estimate. Named openly, not folded silently into the scores. A degraded run also records its exact cause in diagnostics.md.
D1 Cyclomatic Complexity — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. Most of this repository's production source (.cs) had no cyclomatic complexity computed for it: no method bodies were exposed for those file kinds by any language model this pass could load.
D2 Cognitive Complexity — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. Most of this repository's production source (.cs) had no cognitive complexity computed for it: no method bodies were exposed for those file kinds by any language model this pass could load.
D4 Code Duplication — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. Most of this repository's production source (.cs) was not read by duplication detection: no source of those file kinds was exposed to the token comparison by any language model this pass could load.
D9 Test Distribution — measured, with a gap in what it reached — Watchdog measured this, but not all of it. What it did not reach is a gap on our side — a collector, parser or image we have not built yet — so the numbers on that dimension cover less than the repository, and the part left out is not evidence that it would have passed. 1227 test method(s) were found on disk (co-located in feature projects, or outside the analyzed solution) rather than in dedicated test projects, so the unit/integration/E2E pyramid can't be classified — they're counted as one undifferentiated suite. You can widen what we reach: moving the tests into dedicated test projects inside the analyzed solution would let the unit/integration/BDD/E2E split be classified — but the counting gap is ours, so nothing is deducted either way.
D11 Test Reliability — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. Test reliability NOT MEASURED: the test run produced no results for any test tier, so no test ever ran and flakiness could not be exercised. The cause could not be attributed, so it is excluded from the score rather than read as an absence of tests.
D12 Dependency Hygiene — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. No dependency manifest this pass reads for hygiene (a Python pyproject.toml/requirements.txt (pip/uv/Poetry), a Swift Package.swift/Package.resolved, a Cargo manifest, a Go module (go.mod/go.sum), a Gradle version catalogue, a Maven POM, an sbt build (build.sbt), composer.json, package.json, a Dart pubspec.yaml, an Elixir mix.exs/mix.lock (Hex), a rebar.config / erlang.mk DEPS (Hex), a Ruby Gemfile/Gemfile.lock or .gemspec (Bundler/RubyGems)) was found in this repository, so no package was assessed. Zero packages read is NOT a clean dependency tree, so this is NOT SCORED — a gap in the analyzer, not a verdict about this repository. This row is about dependency HYGIENE — outdated, deprecated or unmaintained direct dependencies; known CVEs in the same dependency graph are a separate question, reported under D30 wherever the manifest is OSV-readable.
D14 License Compliance — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. License scan produced no result — the tool ran but its JSON output could not be parsed; the offline NuGet fallback resolved nothing.
D15 Churn × Complexity Hotspots — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. A hotspot is churn × complexity. Churn was measured (2472 line(s) across the 90-day window), but no complexity could be computed for .cs, which is most of this repository's production code — so every churned file would score as complexity 0 and the hotspot list would be empty no matter how tangled the code is. Not scored — this is a gap in the analysis run, not a finding about this repository.
D17 Explicit Debt — measured, with a gap in what it reached — Watchdog measured this, but not all of it. What it did not reach is a gap on our side — a collector, parser or image we have not built yet — so the numbers on that dimension cover less than the repository, and the part left out is not evidence that it would have passed. The 7 deducted marker(s) and the 3.5/KLoC density on this row were taken over this repository's .NET projects ALONE: .cs (56,133 lines, 100% of production source) went unread, because every marker collector on this path is reached through a C# workspace. D17's marker collectors need a compiler we do not have for that language, so none of its nine marker kinds were read there. The debt in that source is UNMEASURED — its absence from the score above is a gap in this analyzer's language coverage, not a finding that the code carries none.
D18 Solution Shape — evaluation did not complete — Dimension evaluation failed — excluded from the score.
D22 Internal API Consistency — not measured this run — This is a true statement about the repository that carries nothing for its owner to act on, so it is reported here rather than as a defect in their code. The loaded project set declares no packable project and no `.Contracts` project, so there is no intentionally-exposed surface for API consistency to be judged over.
D30 Dependency Vulnerabilities — measured, with a gap in what it reached — This is a true statement about the repository that carries nothing for its owner to act on, so it is reported here rather than as a defect in their code. The scanner produced no output at all, so no dependency was actually scanned. 1 of 2 declared ecosystem(s) (npm) WERE scanned and every vulnerability they reported is included in this result; osv was not, so this dimension's score covers less than the dependency surface this repository declares, and nothing here is evidence that osv is free of known-vulnerable dependencies.
D31 IaC & Container Security — measured, with a gap in what it reached — Watchdog measured this, but not all of it. What it did not reach is a gap on our side — a collector, parser or image we have not built yet — so the numbers on that dimension cover less than the repository, and the part left out is not evidence that it would have passed. Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile.meta carries no `FROM` instruction, so it declares no build stage and every Dockerfile rule this engine owns — the runtime-hardening, key-material, mutable-clone, no-op-shim, build-context, trust-anchor, install-guard, setuid and world-writable-path rules — is outside its own premise there and returned nothing. That silence is deliberate and correct: with no base image there is no image to reason about. It is reported here because it is NOT the same fact as a clean file, and coverage is stated as 1 of 2 Infrastructure-as-Code manifest(s) fully judged rather than as 100%. The vendor scanners (trivy, checkov) do read these files and their findings above stand; only this engine's own stage-keyed rules are absent. No owner action: a build file with no FROM is a legitimate include fragment.
D32 Data Compliance (PII/GDPR) — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. `Assets/Mirror/Core/Tools/Utils.cs`, `Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs`, `Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs` produced a parse error, so every rule in this engine's `gdpr.yml` was absent there. That absence is NOT a clean result: these rules detect personal data crossing a boundary into a log sink, a URL or browser storage, and a file that was never parsed cannot report any of the three. The rest of the tree analysed normally and its rows above stand; only these files are unaccounted for. You can widen what we reach: fix the syntax error (or exclude the file deliberately) and re-scan to cover it.
D39 IL Efficiency — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. IL NOT MEASURED: the analyzer's own build of this repository failed for an ENVIRONMENT reason (exit 1) — MSBuild's engine or the CLR gave up, or our image does not carry the SDK band/targeting pack this repository needs. This is OUR limitation, not a defect in the repo, and it is not a statement that this repository fails to build. D18 owns the question of whether this repository builds; it was not answered here.
ED5 Idempotency — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check finds retry-prone mutations by walking the repository's declared types, and NONE was loaded on this run, so it had nothing to look at. That is a limit of the analyzer's reach — it reads .NET projects — not a finding that this repository has no command handlers or message consumers.
X12 Unreachable branch — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X13 Undrained process stream — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X14 Bypassable address classification — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X15 Unvalidated length from an untrusted reader — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X16 Unfloored truncation loop — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X17 Uncapped recursion over a caller-supplied document — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X18 Disposal-pattern correctness — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X19 Unrestored process-global state — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X20 Mistyped argument guard — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X21 Side-effecting pattern guard — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X22 Contradicted release guard — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X23 Unguarded diagnostic materialisation — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X24 Document value interpolated into markup unescaped — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X25 Inert configuration knob — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X26 Unsynchronised callback handoff — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X27 Collection changed while being enumerated — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X28 Index access outside its own emptiness guard — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X29 Per-element action decided by a fixed element — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X30 Support guard that admits what it rejects — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X32 Type resolved by simple name across every loaded assembly — not measured this run — Watchdog could not measure this here. That is a gap on our side — a collector, parser or image we have not built yet — and it is neither a defect in this repository nor evidence that the check would have passed. This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
Solution could not be loaded — run is Degraded — The C# workspace did not read this repository's production source, so every compiler-dependent dimension ran on estimated input. Treat the grade as indicative only; diagnostics.md records the exact cause.
Repo exclusion declarations (.gitattributes linguist-generated/vendored, .editorconfig generated_code): none declared — every source file was scored.
Limitations & what we did not check
Watchdog assesses the repository exactly as committed, and only the repository. By design it does not reach outside the source tree: the live cloud account, the running CI/CD pipeline, the host's branch-protection and approval rules, the production configuration, or a restore actually exercised against a backup are all out of scope. That boundary is a feature, not a gap — a repo-relative, deterministic scan re-runs identically on any commit and every finding opens at a real file and line, where a live audit can neither be reproduced nor traced. The visible consequence is that controls which leave no in-repo evidence are reported as "not evidenced" and excluded from the score rather than awarded a number a static scan cannot justify.
Per-dimension blind spots
For each dimension that was measured, what a static, repo-only scan structurally cannot see — the honest edge of the measurement, not a failure of it.
D5 Coupling: Coupling is measured between projects/assemblies — runtime coupling through DI, reflection, messaging or shared databases is invisible to a static reference graph.
D9 Test Distribution: The test-pyramid shape is inferred from project/folder naming and references, with a single test host bucketed per-file by its path tier and content signals — a suite that names tiers unconventionally and gives no per-file signal can still be mis-bucketed.
D10 Test Quality: Assertion density is structural — it cannot tell a meaningful behavioural assertion from a trivial one, only that an assertion is present.
D13 Secret Scanning: Secret detection is signature- and entropy-based on the current tree — a secret that does not match a known pattern, or one already rotated, will not be flagged (a clean scan is "nothing matched", not "no secrets exist").
D16 Bus Factor: Bus-factor is a time-decayed model of commit attribution (who has recently, repeatedly worked a file), not comprehension — pairing, review and reading-without-committing spread knowledge it can't see; bot commits and shared accounts still distort it.
D17 Explicit Debt: Acknowledged-debt signals (TODO/FIXME, suppressions, dead code) are textual — undocumented debt that nobody marked, and debt that lives in design rather than annotations, is invisible. Committed machine-written code (scaffolded migrations, designer/codegen output, generated stubs) is excluded — it is never the team's dead code to delete.
D19 Documentation Quality: Documentation quality is judged by an LLM over a bounded sample of docs — it reads what is written, not whether the docs match the running system, and it is advisory, not a measurement. Its critique rows are drawn from a closed category vocabulary and each row means the same thing in every run, so two scans can be compared row by row; the SET that fires is still a sample, and does not repeat exactly. Measured on one frozen input, six scans at one engine SHA: 2-5 critique rows per scan, 8 distinct rows across the six, 3 of those 8 seen in only one scan. So a D19 row is evidence about the documentation, but a COUNT of D19 rows is not a quantity — never read a change in it as an improvement or a regression.
D21 Naming Consistency: Naming quality is an LLM judgement over a bounded sample — it assesses clarity/consistency of the names it sees, not domain-correctness, and is advisory.
D24 Comment Value: Comment value (WHY vs WHAT) is an LLM judgement over a bounded sample — it is advisory and cannot weigh a comment against the precise code change it was written to explain.
D26 Project Cohesion: Project focus is sized from members/namespaces per project — a project that is broad by deliberate design reads the same as one that has sprawled.
D28 Secrets (history): Secret-history scanning sweeps the git log for known patterns — a secret that predates the available history, or never matched a signature, is not found (clean means "nothing matched in the history we can see").
D29 Static Analysis (SAST): SAST findings are pattern-based (semgrep) — it finds classes of bug it has rules for; logic flaws, auth/authorization gaps and issues needing runtime context are out of reach (and clean means "no rule matched").
D30 Dependency Vulnerabilities: CVE matching depends on accurate package/version metadata and on the advisory databases — a vulnerability with no published advisory, or in code not declared as a dependency, is not seen. Coverage needs a RESOLVED graph: an unpinned requirements.txt, or a pom without a resolved build, yields partial coverage rather than a clean verdict. An ecosystem the analyzer cannot scan is reported as unmeasured, never as clean.
D31 IaC & Container Security: IaC scanning checks Dockerfiles/Terraform/Kubernetes against best-practice rules — it cannot see the live cloud account, runtime configuration, or drift between the committed config and what is actually deployed.
D34 Knowledge Freshness: Freshness is decayed commit RECENCY, not comprehension — code read often but rarely committed reads as orphaned, and stable code that genuinely needs no changes is penalised the same as forgotten code; bot/squash commits distort it like the bus factor.
D35 Change Coupling: Change coupling is co-change in COMMITS — files split across separate commits, or coupled only through a shared config/build step, read as uncoupled, and a sweeping commit (rename/format) is excluded so it doesn't couple everything. It shows that files change together, not WHY: a high coupling can be a healthy cohesive pair as readily as a hidden leak.
AX10 Code composition: Role is inferred from namespace/folder convention, not semantics — a domain concept living in a folder named "Services" reads as application, and the split is lines-of-code, not business value. The business-logic-share score is a SOFT, FLOORED signal: it contributes to the Architecture lens but is floored at the Critical gate, so an infrastructure-heavy design (a gateway, an ETL, a driver) is legitimately low without being nuked to zero.
M4 Documentation accuracy: Onboarding quality is an LLM read of the docs/setup present — it cannot run the onboarding or measure how long a real new joiner takes; the verdict is sampled and advisory.
P4 Deployment & Rollback: Approval/branch-protection rules live in repository settings the scan cannot see — only their in-repo evidence (config files, workflows) is checked, so a control enforced purely in the host's settings reads as "not evidenced".
P5 DR & Backup: Backup/restore and disaster-recovery readiness is judged from in-repo evidence — a config that exists is not a tested restore, so the absence of positive evidence is reported as "not evidenced", never scored as present.
P6 Release Hygiene: Rollback/observability controls are inferred from repo artefacts (pipelines, dashboards-as-code) — controls configured in external tooling, with no in-repo trace, cannot be credited.
The LLM boundary
LLM-set scores this run (4): D19, D21, D24, M4 (model: Local LLM). For these, a model reads a bounded sample and sets the numeric score; each names its own sample and method on its card. They are sampled and advisory by design: they vary at the margins between runs and are never a deterministic measurement. Every other score in this report is tool-computed at confidence 1.0.
Dimensions
D5 · Coupling6.9 / 10Adequate✓ Tool-verified
What it measures: Whether volatile projects sit underneath others that depend on them (so their churn ripples upward), and whether project dependencies form cycles. A widely-depended-on but stable shared/kernel project is healthy, not penalised.
Method: Dependency cycles via elementary-DFS over real .csproj references, plus Martin instability (afferent/efferent) per project. Exhaustive over the reference graph, deterministic.
Coverage: Exhaustive · type-level: afferent/efferent coupling + cycles computed over every production type — the population is all types, not a name convention.
Enforce Coupling in CI to reach Verified (currently Documented). — Hardens enforcement from Documented toward Prevented — provenance only; does not change the score.
Detailed fixes: d5_recommendation.md.
Do you agree with this assessment?
D9 · Test Distribution10.0 / 10Exemplary✓ Tool-verified
What it measures: Whether the test suite has a healthy mix of unit / integration / end-to-end tests.
Method: Test projects classified (Unit/Integration/BDD/E2E) from compiled metadata; test methods counted exhaustively across projects with placement-agnostic disk fallback. Deterministic.
1227 test methods: 1227 unit, 0 integration, 0 BDD, 0 e2e.
✓ On the Gold path — maintain.
Detailed fixes: d9_recommendation.md.
Do you agree with this assessment?
D10 · Test Quality8.0 / 10Adequategated by 10 critical findings✓ Tool-verified
What it measures: Whether the tests truly assert behaviour rather than just running the code.
Method: Per-test assertions, skips, and mock references analyzed via Roslyn; structured skip-reason tags (BUG:/ENV:) separate documented deferrals from debt. Deterministic.
12 skipped (12 with a documented reason), 2 zero-assertion, 82 excluded from compilation in 10 file(s), no mocking-framework packages referenced (hand-written doubles or no mocking) across 1227 tests.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverClientRpcTests.cs · ×10Assets/Mirror/Tests/Editor/Weaver/WeaverClientRpcTests.cs:7
No assertions: AddMessage · ×2Assets/Mirror/Tests/Editor/Batching/BatcherTests.cs:52
Resolve the 10 Tests excluded from compilation finding(s) in Test Quality — start with WeaverClientRpcTests.cs, WeaverClientServerAttributeTests.cs, WeaverCommandTests.cs. — One of this dimension's main actionable groups (10 issue-level).
Resolve the 2 No assertions finding(s) in Test Quality — start with BatcherTests.cs, ThreadLogTests.cs. — One of this dimension's main actionable groups (2 warning-level).
Enforce Test Quality in CI to reach Verified (currently Documented). — Hardens enforcement from Documented toward Prevented — provenance only; does not change the score.
Detailed fixes: d10_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether any secrets (keys, tokens, passwords) have leaked into the code.
Method: In-process native secret scanner (entropy plus signature patterns) across all tracked files; no external tool. A clean result is a measured 10, not no-data zero. Deterministic.
Resolve the 1 Leaked secret finding(s) in Secret Scanning — start with AppVersionEnv.cs. — One of this dimension's main actionable groups (1 issue-level).
Enforce Secret Scanning in CI to reach Verified (currently Documented). — Hardens enforcement from Documented toward Prevented — provenance only; does not change the score.
Detailed fixes: d13_recommendation.md · top locations in Appendix A, every location in findings.md.
Do you agree with this assessment?
D16 · Bus Factor9.3 / 10Exemplary✓ Tool-verified
What it measures: Whether knowledge is concentrated in too few people (the "bus factor").
Method: Living knowledge per author via time-decayed commit attribution (6-month half-life, focus weighting) across largest source files. Deterministic, avoids blame's mechanical-refactor false positives.
15 source file(s) have their living knowledge concentrated in one author (≥90% of recent, decayed contribution). The largest is Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs. Counted over 217 of the 410 production source files in this repository: the rest are under the ~2,400-byte size floor this dimension measures over.
Off-boarding risk: anonymized user #1
Further sole-owners (lower concentration)
✓ On the Gold path — maintain.
Detailed fixes: d16_recommendation.md · top locations in Appendix A, every location in findings.md.
Do you agree with this assessment?
D17 · Explicit Debt3.0 / 10Weak✓ Tool-verified
What it measures: Acknowledged debt left in the code — TODOs, dead code, suppressed warnings.
Method: Roslyn syntactic debt markers (suppressions/TODO/FIXME/HACK/empty-catch/commented-code/Obsolete) plus SymbolFinder dead-code analysis; weighted-debt-per-KLoC density deducted 2.0x per unit. Deterministic, exhaustive.
7 deducted debt markers + 0 dead symbols across 0 LoC in the .NET projects (3.5/KLoC) → score 3.0. Measured on the .NET source only: .cs (100% of production source) was not read, and carries at least 0 uncounted task marker(s) in 0 file(s).
Resolve the 6 WriteOnlyPrivateField finding(s) in Explicit Debt — start with NetworkBehaviourClientRpcParamAbstract.cs, NetworkBehaviourClientRpcParamComponent.cs, NetworkBehaviourCmdParamAbstract.cs. — One of this dimension's main actionable groups (6 issue-level).
Resolve the 1 NoWarnInCsproj finding(s) in Explicit Debt — start with .WeaverTests.csproj. — One of this dimension's main actionable groups (1 issue-level).
Enforce Explicit Debt in CI to reach Verified (currently Documented). — Hardens enforcement from Documented toward Prevented — provenance only; does not change the score.
Detailed fixes: d17_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether the project's documentation is clear, complete, and useful.
Method: Judged by language model at low temperature (0.0-0.1) on a deterministic doc sample (READMEs plus first 25 architecture docs), with two-pass stability filtering. Advisory, sampled.
Mirror's documentation is comprehensive and well-organized: a single README with links to the full Gitbook docs, an asset-store badge for download, and a dedicated Hosting/Edgegap plugin quickstart. The README itself gives an overview of what Mirror is (an MMO Scale Networking library for Unity) plus installation requirements, usage notes about MonoBehaviours in the Editor folder, and links to every major example repository (VR, TanksHybrid, Room, MultipleMatches). There are no architecture or design docs present, so the missing-architecture-docs gap is not yet filled. The README also contains a clipped clip marker after an Edgegap Relay plugin doc, which does not signal a defect but is consistent with the unshown sections being documented in the outline.
Documentation: no installation or build instructionsAssets/Mirror/Readme.txt
What to do
Resolve the 1 Documentation finding(s) in Documentation Quality — start with Readme.txt. — One of this dimension's main actionable groups (1 recommendation-level).
Detailed fixes: d19_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether names — types, methods, variables — are clear and consistent.
Method: Judged by language model at low temperature (0.0-0.1) on a deterministic random symbol sample (fixed size, not exhaustive), with disclosed confidence band. Advisory, sampled.
1 naming inconsistencies across 200 sampled symbols.
Typo in namespace/type name: 'Virutal' is used instead of the standard spelling 'Virtual'. This appears in both the Client and Server attribute test namespaces/types.
✓ On the Gold path — maintain.
Detailed fixes: d21_recommendation.md · top locations in Appendix A, every location in findings.md.
Do you agree with this assessment?
D24 · Comment ValueWeak◐ Sampled · advisory
What it measures: Whether comments are worth it — explaining WHY (valuable) rather than WHAT (redundant).
Method: Judged by language model at low temperature (0.0-0.1) on deterministically sampled inline comments with surrounding code; findings verified back to sampled comments by substring match. Advisory, sampled.
Resolve the 2 redundant comment finding(s) in Comment Value — start with OverrideAbstractClientRpc.cs, ClientAttributeOnOverrideMethod.cs. — One of this dimension's main actionable groups (2 recommendation-level).
Detailed fixes: d24_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether any secrets were ever committed — scanned across the full git history, not just now.
Method: Secret scan via TWO gitleaks detect passes in an isolated checkout — the full git history, then a second --no-git pass over the working tree as it stands — merged and de-duplicated by (rule, file, line); each match flagged High. Both invocations are recorded in the audit trail. Exhaustive; when the tool is absent, or when its output cannot be parsed into the expected shape, the dimension is WITHHELD as an explicit measurement gap on our side — unscored and excluded from the lens, never a hedged middling score.
4 finding(s): 0 critical, 4 high, 0 medium, 0 low. Remediation for historically-committed secrets is credential rotation — they remain in history regardless of later deletion.
Rotate the exposed credentials — git history can't be un-committed
What to do
Resolve the 4 Secret finding(s) in Secrets (history) — start with ProjectSettings.asset (2), TanksNetworkManager.prefab, NetworkManager.prefab. — One of this dimension's main actionable groups (4 issue-level).
Resolve the 1 Rotate the exposed credentials finding(s) in Secrets (history). — One of this dimension's main actionable groups (1 recommendation-level).
Detailed fixes: d28_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Real static-analysis (SAST) findings — likely security bugs in the code, any language.
Method: Polyglot static analysis via semgrep across the repo using the pinned, image-baked p/security-audit + p/owasp-top-ten rulesets (no scan-time registry fetch); severity rules (ERROR/WARNING/INFO) map to a full-band severity-weighted score. Exhaustive, deterministic; degrades on parse failure.
Coverage: semgrep pattern rules over all files — exhaustive for the rule set, blind to classes of bug without a rule (clean = no rule matched).
26 finding(s): 0 critical, 25 high, 1 medium, 0 low. 10 unpinned-GitHub-Actions row(s) are reported here but scored by D36 (supply-chain provenance), which measures that posture as `pinned_actions` — one pinning decision is charged once, not once per lens. semgrep hit a parse error in 3 file(s) — `Assets/Mirror/Core/Tools/Utils.cs` (line 50), `Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs` (line 79), `Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs` — so no absence of findings in the named regions is evidence of anything; rows reported elsewhere in those files are real. Fix the syntax error (or exclude the file deliberately) and re-scan to cover them. In 1 of those file(s) — `Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs` — the break is at a TYPE DECLARATION (a C# primary constructor semgrep's grammar cannot parse), so the loss is wider than the named lines: rules scoped to that type see no type to scope to and are blind over its whole body, while rules matching statements keep working there. Absence of a type-scoped finding in those types is not evidence of anything.
High: github-actions-mutable-action-tag · ×25.github/workflows/RunUnityTests.yml:24detected by semgrep finding
Medium: watchdog-xmldocument-dtd-entity-expansion-csharpAssets/Mirror/Editor/AndroidManifestHelper.cs:30detected by semgrep finding
What to do
Resolve the 15 High finding(s) charged to Static Analysis (SAST) — the other 10 are reported here at file:line but scored by D36 (supply-chain provenance), which charges them once. — One of this dimension's main actionable groups (25 issue-level, 15 of them charged here).
Resolve the 1 Medium finding(s) in Static Analysis (SAST) — start with AndroidManifestHelper.cs. — One of this dimension's main actionable groups (1 warning-level).
Detailed fixes: d29_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether any dependency has a known published vulnerability (CVE), direct or transitive, in ANY ecosystem the repository declares — Dart pub, Elixir/Hex, Go modules, Java and Kotlin via Maven/Gradle, JavaScript/npm, .NET/NuGet, PHP/Composer, Python/PyPI, RubyGems, Rust/Cargo and Swift.
Method: Dependency-CVE scan across every ecosystem the repository declares, scored ONCE. Three sources are unioned and deduplicated by advisory identity (rule id + alias closure, CVE<->GHSA) scoped to package+version, keeping the worst severity: `osv-scanner --recursive` over osv.dev for Dart pub, Elixir/Hex, Go, Java and Kotlin via Maven/Gradle, npm, PHP/Composer, Python/PyPI, RubyGems, Rust/Cargo and Swift; `trivy fs --scanners vuln` for npm lockfiles; and `dotnet list package --vulnerable --include-transitive` for NuGet (with per-advisory collapse of the project x target-framework fan-out), plus a DECLARED-dependency arm that resolves a published gem's gemspec against rubygems.org where no Gemfile.lock is committed. `SeverityScore(c,h,m,l, normalizer 8.0)`. NotApplicable only when NO ecosystem is readable; if any applicable ecosystem could not be scanned the findings are REPORTED and the score is withheld. Supersedes the npm and OSV arms, retired 2026-09-05.
No known-vulnerable dependencies in the 1 ecosystem(s) that were scanned (npm). Partial dependency scan: 1 of 2 declared ecosystem(s) were scanned (npm), and the findings above are real and complete for them. osv was not scanned (osv: the scanner produced no output at all, so no dependency was actually scanned), so this is not the whole dependency surface and the result is reported at reduced confidence.
Scanner failed to run — not a clean resultdetected by osv-scanner (run failure)
✓ On the Gold path — maintain.
Detailed fixes: d30_recommendation.md · top locations in Appendix A, every location in findings.md.
High IaC: DS-0002 · ×2Assets/Mirror/Hosting/Edgegap/Editor/Dockerfiledetected by trivy finding
Medium IaC: WD-DOCKER-0003 · ×3Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile:1detected by watchdog IaC rule
What to do
Resolve the 2 High IaC finding(s) in IaC & Container Security — start with Dockerfile (2). — One of this dimension's main actionable groups (2 issue-level).
Resolve the 3 Medium IaC finding(s) in IaC & Container Security — start with Dockerfile.meta (2), Dockerfile. — One of this dimension's main actionable groups (3 warning-level).
Detailed fixes: d31_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether anyone still has living knowledge of each file, or it has been orphaned — last understood long ago by someone now gone quiet. The sibling of the bus factor: D16 asks who owns it, D34 asks whether anyone still knows it.
Method: File orphaning as total living-knowledge decay below one focused-commit's worth within a year, computed per-file from the D16 decay model. Exhaustive, deterministic over fixed history.
17 of 217 significant source file(s) are orphaned — their living knowledge has decayed to nothing, so no one currently understands them. The largest is Assets/Mirror/Transports/Encryption/EncryptedConnection.cs. Counted over 217 of the 410 production source files in this repository: the rest are under the ~2,400-byte size floor this dimension measures over.
Resolve the 1 Orphaned knowledge finding(s) in Knowledge Freshness — start with EncryptedConnection.cs. — One of this dimension's main actionable groups (1 issue-level).
Resolve the 1 Further orphaned files (smaller) finding(s) in Knowledge Freshness. — One of this dimension's main actionable groups (1 recommendation-level).
Detailed fixes: d34_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether files that change together actually belong together — pairs that repeatedly co-change in git history despite having no explicit code dependency, surfacing the hidden/logical coupling (and boundaries in the wrong place) a static scan can't see.
Method: Pairwise co-occurrence over the per-commit file sets in git history (production source only — tests and generated dropped): Degree-of-Coupling = shared ÷ min individual revisions, reported above noise floors (each file ≥10 revisions, ≥5 shared commits, ≥50% strength); sweeping commits excluded. Deterministic over fixed history.
Coverage: Population: PRODUCTION source files only — test and generated files are dropped before pairing, so a class co-changing with its own test (trivially ~100%) can't drown the real production↔production coupling. Pairs ranked by Degree-of-Coupling. A non-source file is never a coupling PARTICIPANT either: documentation, schemas, config and data files are dropped with the rest, so a code↔docs pair — a command and the reference page that restates it — is not reported however strongly the two co-change; nor is coupling that runs THROUGH a build step or config file.
Resolve the 4 Change coupling finding(s) in Change Coupling — start with Batcher.cs (2), KcpClient.cs, NetworkReaderPool.cs. — One of this dimension's main actionable groups (4 warning-level).
Resolve the 1 Change coupling clique finding(s) in Change Coupling — start with CommandProcessor.cs. — One of this dimension's main actionable groups (1 warning-level).
Detailed fixes: d35_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether the build pipeline provides supply-chain integrity — generated provenance/attestation, signed artifacts (cosign/sigstore), an SBOM, and pinned build actions. Presence of the configuration, not a runtime guarantee.
Method: Supply-chain provenance/signing read deterministically from CI/build config (.github/workflows, .gitlab-ci.yml, azure-pipelines, Jenkinsfile, .circleci) + the release surface: four signals — generated provenance/attestation (SLSA/in-toto/actions-attest), artifact signing (cosign/sigstore/gitsign), an SBOM (syft/sbom-action/*.spdx.json/*.cdx.json), and SHA-pinned build actions — scored 10·present/denom. NotApplicable without a build pipeline. Detects configuration presence, not runtime enforcement.
+ 2 more group(s) — more in Appendix A; the complete list is findings.md.
What to do
Resolve the 1 Unpinned build actions finding(s) in Supply-chain Provenance & Signing. — One of this dimension's main actionable groups (1 warning-level).
Resolve the 1 PR-triggered workflow without a permissions block finding(s) in Supply-chain Provenance & Signing. — One of this dimension's main actionable groups (1 warning-level).
Resolve the 1 Workflow holding a long-lived secret is unscoped finding(s) in Supply-chain Provenance & Signing. — One of this dimension's main actionable groups (1 warning-level).
Detailed fixes: d36_recommendation.md · top locations in Appendix A, every location in findings.md.
What it measures: Whether the repository publishes a coordinated-vulnerability-disclosure policy (SECURITY.md or security.txt) with a reporting contact, so finders know how to report a vulnerability. Presence of a policy file with a contact, not whether the policy is adequate or honoured.
Method: Vulnerability-disclosure policy read deterministically from the repo: a SECURITY.md (root/.github/docs) or .well-known/security.txt / security.txt, regex-checked for a reporting contact (email / URL / mailto). Present + contact → 10; present without a contact → 4; NotApplicable when no policy file exists (it may live off-repo). Detects the policy file's presence + contact, not its adequacy.
Other · Architecture — How the codebase splits by code ROLE — domain, application, infrastructure, test, generated. The significance map behind the knowledge/coupling weighting, and a DDD signal in its own right: a thin domain core under fat infrastructure is the anemic-domain smell, quantified. How each file's role is decided, because the split is only as good as that: a generated name or a build-output tree makes it Generated, a test project makes it Test, and otherwise the file's NAMESPACE and PATH words are matched against fixed vocabularies in a fixed ORDER — domain, then infrastructure, then application — so a file whose words hit two layers is counted under the earlier one. A production file matching none of them counts as application, so that share reads 'application or unclassified' rather than a measured application layer. Roles come from naming convention, never from what the code does.
Method: Roslyn line-count by code ROLE: every source file classified Domain/Application/Infrastructure/Test/Generated by namespace + path convention (the shared CodeRoleClassifier), then significant lines summed per role. Deterministic; the advisory score is the business-logic (domain+application) share of production code.
Coverage: Population: ALL source files, each bucketed into ONE of five roles (Domain/Application/Infrastructure/Test/Generated) by namespace + path convention — a file whose layer isn't named in the convention falls to Application (the neutral default), and the split is line-count, not semantic depth or business value.
Other · Architecture — Whether the project-reference graph is acyclic (cycles block independent build/deploy and signal eroding boundaries).
Method: Project reference cycles via elementary-DFS over real .csproj references, using the engine shared with D5/D7; cyclic versus acyclic. Exhaustive, deterministic.
Other · Architecture — Whether dependencies point inward (Domain ← Application ← Infrastructure/Web) — the clean-architecture dependency rule, checked across the project graph.
Method: Layer violations by name-segment inference (Domain/Core to Application to Infrastructure/Web) over the project-reference graph. Exhaustive over all projects, deterministic.
Maturity · Maturity — Whether the repo and its projects have a README, and whether it's substantive and current.
Method: Filesystem scan: README presence, word count, and headings for depth; git history for staleness. Exhaustive across root and project dirs, deterministic.
What to do
Add a 'Testing' section to the root README — how to run the test suite.
Maturity · Maturity — Whether key decisions (ADRs) and the high-level shape (C4/diagrams) are written down.
Method: Filesystem scan: ADR folder/naming conventions or content, plus Mermaid/PlantUML/C4/architecture.md discovery. Exhaustive, deterministic.
No Architecture Decision Records found — no conventional ADR directory, no numbered `NNNN-title` documents in any markup this check reads, and nothing ADR-shaped by content. Design rationale recorded elsewhere (a design-notes tree, a mailing list, pull-request discussion) is not visible to this check and is not re-findable per decision, so a future maintainer cannot ask why one choice was made and get an answer.
What to do
Record significant decisions one document per decision — dated, stating the context, the decision and its consequences — and keep them together wherever your design docs already live (a conventional `docs/adr/` tree, with each file named `NNNN-title` in whatever markup those docs already use, is the most discoverable form).
Maturity · Maturity — Whether the README actually describes the code that exists (LLM-judged, advisory).
Method: Judged by language model at low temperature: README accuracy versus actual projects, within a disclosed tolerance. Advisory, not a measured number.
Readiness · Readiness — Whether SAST, secret/dependency scanning and performance benchmarking are wired in (presence, not runtime).
Method: Filesystem scan: SAST configuration, dependency-update automation, secret scanning, and a benchmark harness or benchmark step — in this repository's own ecosystem. Exhaustive, deterministic.
What to do
Enable Dependabot/Renovate or a dependency-review gate.
Add gitleaks/trufflehog in CI to block PRs that introduce committed secrets.
Readiness · Readiness — Whether releases are automated and safely reversible (probes, rolling updates, approval gates) — from manifests/pipeline files, not the live environment.
Method: Filesystem scan: deployment manifests/IaC (K8s YAML, Helm, Terraform) for rolling updates, probes, approval gates, migration hooks. Exhaustive, deterministic.
What to do
Nothing pauses a release for a human: publish as a draft release (or gate the release job on a protected tag/manual dispatch) so a bad build can be stopped before users can download it.
Do you agree with this assessment?
P5 · DR & Backup3.0 / 10Weak✓ Tool-verified
Readiness · Readiness — Whether disaster recovery is planned and codified — backups, geo-recovery, RTO/RPO, persistence guarantees — from IaC + container manifests + docs, never the live cloud.
Method: Filesystem scan: disaster recovery, backup, geo-recovery, RTO/RPO, persistence guarantees from IaC, manifests, and docs. Exhaustive, deterministic, never a live environment.
What to do
Codify backups + geo-recovery in IaC (snapshot/replication/geo-redundant) to back the DR plan.
Enable purge protection / soft-delete (and prevent_destroy on critical resources) so data stores can't be lost to an accidental or malicious delete.
Readiness · Readiness — Whether releases are traceable — a maintained changelog and explicit version stamping.
Method: Filesystem scan: changelog file presence and version tags in csproj or git tags. Exhaustive, deterministic.
Do you agree with this assessment?
Reference — by lens
The score is the rank-weighted fold of these lenses (worst-heaviest), each including its meta-dimensions; a lens with a Critical contributor is capped at Fair (its band reads "gated by …") and is never the strongest area however high its average.
Capped at Fair by a Critical contributor — resolve it before relying on this lens.
Not evidenced — 3 control(s) we could not find positive evidence for
These checks grade a working control, and the repository shows no evidence of one. That is deliberately not scored as a zero: a repository cannot show an ops runbook, a database TTL or an infrastructure-side audit log, so absence of evidence here is not evidence the control is missing. It is also not a statement that the check is irrelevant to this codebase — the thing it grades applies; we just could not see it. Excluded from the score either way.
C3 Audit Trail — Not assessed: these audit controls are read from a source model (declarative annotations, request middleware, entity/column names, guard methods) that was not loaded for this repository — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of an idiom this check recognises is NOT evidence that this repository lacks audit controls: it may implement them entirely in its own ecosystem. This is a gap in the analyzer's language coverage, not a finding about this repository.
C4 Data Retention — Not assessed: these retention controls are read from a source model (declarative annotations, request middleware, entity/column names, guard methods) that was not loaded for this repository — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of an idiom this check recognises is NOT evidence that this repository lacks retention controls: it may implement them entirely in its own ecosystem. This is a gap in the analyzer's language coverage, not a finding about this repository.
C5 Data-Subject Rights — Not assessed: these data-subject rights controls are read from a source model (declarative annotations, request middleware, entity/column names, guard methods) that was not loaded for this repository — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of an idiom this check recognises is NOT evidence that this repository lacks data-subject rights controls: it may implement them entirely in its own ecosystem. This is a gap in the analyzer's language coverage, not a finding about this repository.
Not included — 80 check(s) not relevant to this codebase
These checks had nothing to measure here (no tests, no git history, the codebase is small, or the architecture style doesn't apply), so they're omitted above rather than scored low.
AC1 Text alternatives — No web markup found — accessibility is not applicable to this repository.
AC2 Forms & labels — No web markup found — accessibility is not applicable to this repository.
AC3 Page structure — No web markup found — accessibility is not applicable to this repository.
AC4 Keyboard semantics — No web markup found — accessibility is not applicable to this repository.
AC5 ARIA correctness — No web markup found — accessibility is not applicable to this repository.
AC6 Visual & motion safety — No web markup found — accessibility is not applicable to this repository.
AC7 A11y enforcement — No web markup found — accessibility is not applicable to this repository.
AX1 Captive dependencies — no DI registrations detected
AX2 Stateful singletons — no singleton implementations detected
AX5 Architecture & structure — not assessed — architecture style/structure is computed from a project graph (projects, types, module namespaces) that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
AX6 Interface segregation — not assessed — interface segregation is computed over a type surface that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
AX7 Slice cohesion — not applicable — not a vertical-slice architecture
AX8 Test isolation — no test/production split to check
AX9 CQS / query purity — no CQRS query handlers detected — query purity is not applicable to this codebase
AXB2 Runtime readiness — Advisory — this card reports evidence and never carries a score, so there is nothing missing here.
C1 Data Protection — Not assessed: these personal data controls are read from a source model (declarative annotations, request middleware, entity/column names, guard methods) that was not loaded for this repository — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of an idiom this check recognises is NOT evidence that this repository lacks personal data controls: it may implement them entirely in its own ecosystem. This is a gap in the analyzer's language coverage, not a finding about this repository.
C2 Access Controls — Not assessed: these authorization controls are read from a source model (declarative annotations, request middleware, entity/column names, guard methods) that was not loaded for this repository — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of an idiom this check recognises is NOT evidence that this repository lacks authorization controls: it may implement them entirely in its own ecosystem. This is a gap in the analyzer's language coverage, not a finding about this repository.
D1 Cyclomatic Complexity — cyclomatic complexity not measured — .cs exposed no member bodies
D11 Test Reliability — Test reliability not measured — no test run produced results
D12 Dependency Hygiene — Dependency hygiene not measured — no supported dependency manifest was read
D14 License Compliance — Licence collector did not produce a result
D15 Churn × Complexity Hotspots — complexity unreadable for .cs — churn × complexity hotspots could not be measured
D18 Solution Shape — Dimension evaluation failed
D2 Cognitive Complexity — cognitive complexity not measured — .cs exposed no member bodies
D20 ADR Quality — N/A — ADRs are expected on deployable products with a user-facing host, not consumed libraries; no ADR log is required here.
D22 Internal API Consistency — No intentionally-exposed public API to evaluate for consistency.
D23 Boundary Type-Coupling — Zero production LoC and one project mean this is a tiny single-purpose codebase that requires no boundaries. No bounded-context organisation was detected either — neither a context-shaped layout nor 2+ sibling source directories each declaring an aggregate root. Declaring this codebase's bounded contexts (≥2) would let cross-boundary type coupling be assessed. Declare them in `.codehealth/config.yaml` at the repository root (create it if absent), mapping each context name to the module-path or namespace prefixes that belong to it — e.g. `architecture:` → `contexts:` → `Billing: ["src/billing", "Acme.Billing"]`, `Catalog: ["src/catalog", "Acme.Catalog"]`.
D25 ADR Conformance — no ADRs to check
D27 Navigability — symbol resolution incomplete — too few calls resolved to assess navigability
D3 God Classes — Most of this repository's production source (.cs) was not read by god-class detection, so class size was not assessed for the languages that are the product — whatever else this pass did read is not this repository's class size. Not scored — this is a gap in the analyzer, not a verdict about this repository.
D32 Data Compliance (PII/GDPR) — 3 file(s) were not parsed by semgrep — the PII/GDPR ruleset never ran over them
D39 IL Efficiency — IL not measured — the analyzer's build of the target did not succeed
D4 Code Duplication — Duplication not measured — .cs not exposed to the token comparison
D40 Network Egress Confinement — No Kubernetes/orchestration workloads found in the repository manifests; network egress policy is a cluster-native control that may live at the platform/firewall layer, so there is nothing to assess here.
D41 Kernel & Syscall Confinement — No Kubernetes/orchestration workloads found in the repository manifests; seccomp/AppArmor/SELinux confinement is a workload-level control, so there is nothing to assess here.
D42 Runtime Threat Enforcement — No Kubernetes/orchestration workloads found in the repository manifests; runtime threat-detection and admission-control policy are cluster-level controls, so there is nothing to assess here.
D6 Cohesion (LCOM4) — No production classes were analyzable, so cohesion (LCOM4) was not measured (the solution likely failed to load or has no production code).
D7 Architectural Integrity — no checkable ADRs and no dependency cycles — architectural integrity not assessed
D8 Code Coverage — Coverage NOT MEASURED: `--collect:"XPlat Code Coverage"` names a data collector that ships in the `coverlet.collector` package, and this repository wires up none — no test project references it and no runsettings declares one. The absence of coverage here is therefore not evidence about the suite or about our analyzer environment: without a collector, `--collect` produces nothing even from a suite that builds and passes. Add a `coverlet.collector` PackageReference to the test project(s) (or commit the Cobertura/OpenCover/lcov report your CI produces) and real coverage will be measured. It is excluded from the score rather than counted as a near-zero defect.
DM1 Domain Modelling — not scored — this repository shows none of the 3 signals this check looks for
ED1 Event-Driven — not scored — this repository shows none of the 3 signals this check looks for
ED5 Idempotency — This check finds retry-prone mutations (command handlers and message/event consumers) by walking the repository's declared types, and none was loaded here, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
ES1 Event Sourcing — not scored — this repository shows none of the 3 signals this check looks for
GD1 Unfinished & placeholder code — no source files
IC1 Incompleteness & stubs — reported, not scored — this repository's C# declares no analysable method bodies (a constants, record or DTO assembly), and this score is a density of unfinished work per method, which has no denominator here. The file-level signals below were still collected and are shown in full
P12 CI test-gate honesty — Reported, not scored — this card publishes what the CI gate does with the test inventory rather than grading it. The findings above are its output.
P2 Observability — Observability was not assessed: this check reads a source model that does not carry this repository's product — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of a logging idiom this check recognises is NOT evidence that this repo lacks structured logging (it may log through its own ecosystem's logger). This is a gap in the analyzer, not a finding about this repository.
P7 Outbound HTTP resilience — not applicable — this isn't a service/API/worker
P8 Schema migrations — no EF Core usage detected
P9 Domain vs controller coverage — no coverage report found on disk — produce a coverage report in a standard format (Cobertura — `dotnet test --collect:"XPlat Code Coverage"` with a `coverlet.collector` PackageReference) into the repo working tree before the scan — a CI step is the usual place, since the artefact is commonly gitignored, or wire coverage collection into CI, to enable this cross-layer check
PF1 Benchmark discipline — Performance was not assessed: this lens reads a source model that was not loaded for this repository, because the repository is written in a language this lens does not yet model or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository — in particular it is NOT a statement that this repo is unpackaged or performance-careless.
PF2 Allocation hygiene — Performance was not assessed: this lens reads a source model that was not loaded for this repository, because the repository is written in a language this lens does not yet model or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository — in particular it is NOT a statement that this repo is unpackaged or performance-careless.
PF3 Async & latency hygiene — Performance was not assessed: this lens reads a source model that was not loaded for this repository, because the repository is written in a language this lens does not yet model or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository — in particular it is NOT a statement that this repo is unpackaged or performance-careless.
S1 Web-Security Posture — Not assessed: these web-security controls are read from a source model (declarative annotations, request middleware, entity/column names, guard methods) that was not loaded for this repository — because the repository is written in a language this check does not yet model, or because its projects failed to load. Absence of an idiom this check recognises is NOT evidence that this repository lacks web-security controls: it may implement them entirely in its own ecosystem. This is a gap in the analyzer's language coverage, not a finding about this repository.
SC1 Supply-chain hygiene — Advisory — this card reports evidence and never carries a score, so there is nothing missing here.
X1 Async correctness — not analysed — these correctness checks read a source model that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
X12 Unreachable branch — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X13 Undrained process stream — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X14 Bypassable address classification — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X15 Unvalidated length from an untrusted reader — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X16 Unfloored truncation loop — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X17 Uncapped recursion over a caller-supplied document — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X18 Disposal-pattern correctness — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X19 Unrestored process-global state — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X2 Cancellation propagation — not analysed — these correctness checks read a source model that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
X20 Mistyped argument guard — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X21 Side-effecting pattern guard — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X22 Contradicted release guard — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X23 Unguarded diagnostic materialisation — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X24 Document value interpolated into markup unescaped — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X25 Inert configuration knob — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X26 Unsynchronised callback handoff — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X27 Collection changed while being enumerated — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X28 Index access outside its own emptiness guard — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X29 Per-element action decided by a fixed element — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X3 Exception handling — not analysed — these correctness checks read a source model that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
X30 Support guard that admits what it rejects — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X32 Type resolved by simple name across every loaded assembly — This check reads C# syntax; no C# was loaded for this repository, so it has nothing to report. That is a limit of the analyzer, not a finding about your code.
X4 Structured logging — not analysed — these correctness checks read a source model that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
X5 Nullable reference types — not analysed — these correctness checks read a source model that was not loaded for this repository, because the repository is written in a language this check does not yet model, or because its projects failed to load. This is a gap in the analyzer, not a finding about this repository
Appendix A — Findings (grouped)
The findings behind the scores, grouped by severity, then by dimension and kind. The high-severity issues are enumerated in full below; items per group are capped at 25 with any overflow stated explicitly per group, never silently truncated. The complete machine-readable list of every finding (all severities) is the companion findings.md in this report's bundle.
High: github-actions-mutable-action-tag .github/workflows/RunUnityTests.yml:24— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: actions/checkout@<40-character SHA>`. This step references `actions/checkout@v7`; resolve the SHA it points at today with `gh api repos/actions/checkout/commits/v7 --jq .sha`.
High: github-actions-mutable-action-tag .github/workflows/RunUnityTests.yml:36— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: game-ci/unity-test-runner@<40-character SHA>`. This step references `game-ci/unity-test-runner@v4`; resolve the SHA it points at today with `gh api repos/game-ci/unity-test-runner/commits/v4 --jq .sha`.
High: watchdog-secret-into-unpinned-action .github/workflows/RunUnityTests.yml:41— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: watchdog-secret-into-unpinned-action .github/workflows/RunUnityTests.yml:42— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: watchdog-secret-into-unpinned-action .github/workflows/RunUnityTests.yml:43— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: github-actions-mutable-action-tag .github/workflows/RunUnityTests.yml:56— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: actions/upload-artifact@<40-character SHA>`. This step references `actions/upload-artifact@v7`; resolve the SHA it points at today with `gh api repos/actions/upload-artifact/commits/v7 --jq .sha`.
High: github-actions-mutable-action-tag .github/workflows/RunUnityTests.yml:63— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: actions/upload-artifact@<40-character SHA>`. This step references `actions/upload-artifact@v7`; resolve the SHA it points at today with `gh api repos/actions/upload-artifact/commits/v7 --jq .sha`.
High: github-actions-mutable-action-tag .github/workflows/RunUnityTests.yml:71— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: codecov/codecov-action@<40-character SHA>`. This step references `codecov/codecov-action@v7`; resolve the SHA it points at today with `gh api repos/codecov/codecov-action/commits/v7 --jq .sha`.
High: watchdog-secret-into-unpinned-action .github/workflows/RunUnityTests.yml:76— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: github-actions-mutable-action-tag .github/workflows/RunUnityTests.yml:80— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: MirrorNetworking/nunit-reporter@<40-character SHA>`. This step references `MirrorNetworking/nunit-reporter@master`; resolve the SHA it points at today with `gh api repos/MirrorNetworking/nunit-reporter/commits/master --jq .sha`.
High: watchdog-secret-into-unpinned-action .github/workflows/RunUnityTests.yml:85— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: github-actions-mutable-action-tag .github/workflows/Semantic.yml:16— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: actions/checkout@<40-character SHA>`. This step references `actions/checkout@v7`; resolve the SHA it points at today with `gh api repos/actions/checkout/commits/v7 --jq .sha`.
High: watchdog-unpinned-package-install-in-run .github/workflows/Semantic.yml:21— This CI step installs a third-party package by name with no version, so what lands on the runner is whatever the registry serves at the moment the job happens to run. A global or tool install resolves outside every lockfile in this repository, so nothing in version control records or constrains what was installed, and two runs of an UNCHANGED workflow file can install different code. That matters more than reproducibility here: package managers execute the downloaded package's install-time scripts with the job's environment in scope, tokens included, so a compromised release or a taken-over maintainer account upstream becomes code execution inside this job — the same supply-chain exposure as a floating action reference, reached through the `run:` key instead. Name an exact version on this command, in your manager's own syntax (`pkg@1.2.3` for npm/pnpm/yarn, `pkg==1.2.3` for pip, `-v 1.2.3` for gem, `--version 1.2.3` for cargo and `dotnet tool`, a real `@v1.2.3` module version for `go install` and `go run`), or move the tool into a manifest the repository already pins and installs from — a devDependency plus `npm ci`, a `requirements.txt` fed to `pip install -r`, or a `dotnet-tools.json` restored with `dotnet tool restore`.
High: github-actions-mutable-action-tag .github/workflows/Semantic.yml:26— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: actions/setup-node@<40-character SHA>`. This step references `actions/setup-node@v7`; resolve the SHA it points at today with `gh api repos/actions/setup-node/commits/v7 --jq .sha`.
High: watchdog-mutable-job-container-image .github/workflows/SonarQube.yml:14— This job runs inside a third-party CONTAINER image named by a mutable reference. This is strictly wider than a single step pulling an image: EVERY step in the job — the checkout, the build, the publish, and whatever secrets any of them are handed — executes inside this image. A tag is repointable by the image owner at any moment and carries no immutability at all, so what this job runs can change without a single commit to this repository. `:latest`, an absent tag and a pinned-looking version tag are all the same mutable pointer. Pin the image to its DIGEST — resolve it with `docker buildx imagetools inspect <image>` and write `image: <image>@sha256:<64-character digest>`.
High: github-actions-mutable-action-tag .github/workflows/SonarQube.yml:18— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: MirrorNetworking/unity-runner@<40-character SHA>`. This step references `MirrorNetworking/unity-runner@2.0.0`; resolve the SHA it points at today with `gh api repos/MirrorNetworking/unity-runner/commits/2.0.0 --jq .sha`. Note that `2.0.0` is an exact release tag rather than a floating major: it is still mutable (a tag can be repointed), but by convention it moves only on a force-push.
High: watchdog-secret-into-unpinned-action .github/workflows/SonarQube.yml:36— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: watchdog-secret-into-unpinned-action .github/workflows/SonarQube.yml:37— This step hands a repository secret to a third-party action that is NOT pinned to an immutable reference. A tag - `@v1`, `@v1.2.2`, `@main` - is a pointer the action's owner can repoint at any moment, so the code that receives this secret is whatever that owner publishes next, and it can change without a single commit to this repository or any review by you. An action runs in the job's own process: it can read every secret bound to the step, everything in the workspace, and the runner's network. Pin the action to a full 40-character commit SHA, which names one immutable tree - `uses: owner/repo@<40-character SHA>`, resolved with `gh api repos/<owner>/<repo>/commits/<ref> --jq .sha` - and review the diff before you move it. Where the secret is a long-lived personal access token, scope it down or replace it with the per-run `GITHUB_TOKEN` as well: pinning bounds WHICH code runs, not what the credential can do.
High: secrets-inherit .github/workflows/main.yml:40— This workflow uses `secrets: inherit` to pass all of the calling workflow's secrets to a reusable workflow. This violates the principle of least privilege because the called workflow receives access to every secret in the repository, not just the ones it needs. If the called workflow is compromised or sourced from a third party, an attacker gains access to all repository secrets. Instead, explicitly pass only the secrets that the called workflow requires using the `secrets:` map, e.g. `secrets: { MY_SECRET: ${{ secrets.MY_SECRET }} }`.
High: secrets-inherit .github/workflows/main.yml:53— This workflow uses `secrets: inherit` to pass all of the calling workflow's secrets to a reusable workflow. This violates the principle of least privilege because the called workflow receives access to every secret in the repository, not just the ones it needs. If the called workflow is compromised or sourced from a third party, an attacker gains access to all repository secrets. Instead, explicitly pass only the secrets that the called workflow requires using the `secrets:` map, e.g. `secrets: { MY_SECRET: ${{ secrets.MY_SECRET }} }`.
High: github-actions-mutable-action-tag .github/workflows/main.yml:60— GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. `uses: Mattraks/delete-workflow-runs@<40-character SHA>`. This step references `Mattraks/delete-workflow-runs@v2`; resolve the SHA it points at today with `gh api repos/Mattraks/delete-workflow-runs/commits/v2 --jq .sha`.
High: watchdog-quoted-interpolation-into-process-arguments-csharp Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs:254— A value is interpolated INSIDE a double-quoted span of the argument string this code hands to an external program, so those two quotes are the only thing holding the value together as one argument. .NET passes `ProcessStartInfo.Arguments` to the operating system as a SINGLE string and the program on the other end parses it back into arguments itself — so one double quote in the value closes the span early and the rest of it is read by the receiving program as command line structure rather than as data: the tail of a file name becomes a second argument, an argument becomes a switch, and on a tool that chooses its output path, its input or its plugins from its own switches that is enough to redirect what the call does. Setting `UseShellExecute = false` does not answer this. It removes the shell, and with it the metacharacters a shell would have interpreted, but the split into arguments happens after that point and inside the callee's own parser, which no setting on this side reaches. The wrapping quotes make it look handled, which is why it survives review — but quotes only contain a value that cannot contain the quote, and nothing here established that. Escaping is not the fix to reach for first: pass the arguments as a LIST instead. Add one element per argument to `ProcessStartInfo.ArgumentList` and leave `Arguments` unassigned; the runtime then applies the platform's own quoting to each element, so no content in a value can merge two arguments or split one. The quotes come out with the value, because an element is a whole argument and not a fragment of text. Where the interpolation is genuinely a switch NAME rather than a value, and so cannot be one element of data, that is a different control: check it against a fixed allow-list of the switches this code may pass before it reaches the command line. Where a single argument string is genuinely forced — an API that accepts nothing else — the minimum that makes it safe is one helper that applies the receiving platform's quoting rules over exactly the span it appended, called at every site, and one site that forgets it puts the whole command line back where it started, which is the reason the list form is the recommendation and quoting by hand is the fallback.
High: watchdog-quoted-interpolation-into-process-arguments-csharp Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs:408— A value is interpolated INSIDE a double-quoted span of the argument string this code hands to an external program, so those two quotes are the only thing holding the value together as one argument. .NET passes `ProcessStartInfo.Arguments` to the operating system as a SINGLE string and the program on the other end parses it back into arguments itself — so one double quote in the value closes the span early and the rest of it is read by the receiving program as command line structure rather than as data: the tail of a file name becomes a second argument, an argument becomes a switch, and on a tool that chooses its output path, its input or its plugins from its own switches that is enough to redirect what the call does. Setting `UseShellExecute = false` does not answer this. It removes the shell, and with it the metacharacters a shell would have interpreted, but the split into arguments happens after that point and inside the callee's own parser, which no setting on this side reaches. The wrapping quotes make it look handled, which is why it survives review — but quotes only contain a value that cannot contain the quote, and nothing here established that. Escaping is not the fix to reach for first: pass the arguments as a LIST instead. Add one element per argument to `ProcessStartInfo.ArgumentList` and leave `Arguments` unassigned; the runtime then applies the platform's own quoting to each element, so no content in a value can merge two arguments or split one. The quotes come out with the value, because an element is a whole argument and not a fragment of text. Where the interpolation is genuinely a switch NAME rather than a value, and so cannot be one element of data, that is a different control: check it against a fixed allow-list of the switches this code may pass before it reaches the command line. Where a single argument string is genuinely forced — an API that accepts nothing else — the minimum that makes it safe is one helper that applies the receiving platform's quoting rules over exactly the span it appended, called at every site, and one site that forgets it puts the whole command line back where it started, which is the reason the list form is the recommendation and quoting by hand is the fallback.
High: watchdog-quoted-interpolation-into-process-arguments-csharp Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs:1230— A value is interpolated INSIDE a double-quoted span of the argument string this code hands to an external program, so those two quotes are the only thing holding the value together as one argument. .NET passes `ProcessStartInfo.Arguments` to the operating system as a SINGLE string and the program on the other end parses it back into arguments itself — so one double quote in the value closes the span early and the rest of it is read by the receiving program as command line structure rather than as data: the tail of a file name becomes a second argument, an argument becomes a switch, and on a tool that chooses its output path, its input or its plugins from its own switches that is enough to redirect what the call does. Setting `UseShellExecute = false` does not answer this. It removes the shell, and with it the metacharacters a shell would have interpreted, but the split into arguments happens after that point and inside the callee's own parser, which no setting on this side reaches. The wrapping quotes make it look handled, which is why it survives review — but quotes only contain a value that cannot contain the quote, and nothing here established that. Escaping is not the fix to reach for first: pass the arguments as a LIST instead. Add one element per argument to `ProcessStartInfo.ArgumentList` and leave `Arguments` unassigned; the runtime then applies the platform's own quoting to each element, so no content in a value can merge two arguments or split one. The quotes come out with the value, because an element is a whole argument and not a fragment of text. Where the interpolation is genuinely a switch NAME rather than a value, and so cannot be one element of data, that is a different control: check it against a fixed allow-list of the switches this code may pass before it reaches the command line. Where a single argument string is genuinely forced — an API that accepts nothing else — the minimum that makes it safe is one helper that applies the receiving platform's quoting rules over exactly the span it appended, called at every site, and one site that forgets it puts the whole command line back where it started, which is the reason the list form is the recommendation and quoting by hand is the fallback.
High: watchdog-quoted-interpolation-into-process-arguments-csharp Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs:1235— A value is interpolated INSIDE a double-quoted span of the argument string this code hands to an external program, so those two quotes are the only thing holding the value together as one argument. .NET passes `ProcessStartInfo.Arguments` to the operating system as a SINGLE string and the program on the other end parses it back into arguments itself — so one double quote in the value closes the span early and the rest of it is read by the receiving program as command line structure rather than as data: the tail of a file name becomes a second argument, an argument becomes a switch, and on a tool that chooses its output path, its input or its plugins from its own switches that is enough to redirect what the call does. Setting `UseShellExecute = false` does not answer this. It removes the shell, and with it the metacharacters a shell would have interpreted, but the split into arguments happens after that point and inside the callee's own parser, which no setting on this side reaches. The wrapping quotes make it look handled, which is why it survives review — but quotes only contain a value that cannot contain the quote, and nothing here established that. Escaping is not the fix to reach for first: pass the arguments as a LIST instead. Add one element per argument to `ProcessStartInfo.ArgumentList` and leave `Arguments` unassigned; the runtime then applies the platform's own quoting to each element, so no content in a value can merge two arguments or split one. The quotes come out with the value, because an element is a whole argument and not a fragment of text. Where the interpolation is genuinely a switch NAME rather than a value, and so cannot be one element of data, that is a different control: check it against a fixed allow-list of the switches this code may pass before it reaches the command line. Where a single argument string is genuinely forced — an API that accepts nothing else — the minimum that makes it safe is one helper that applies the receiving platform's quoting rules over exactly the span it appended, called at every site, and one site that forgets it puts the whole command line back where it started, which is the reason the list form is the recommendation and quoting by hand is the fallback.
D10 · Test Quality· Tests excluded from compilation · ×10
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverClientRpcTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverClientRpcTests.cs:7— 3 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverClientRpcTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverClientServerAttributeTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverClientServerAttributeTests.cs:15— 14 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverClientServerAttributeTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverCommandTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverCommandTests.cs:7— 5 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverCommandTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverGeneratedReaderWriterTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverGeneratedReaderWriterTests.cs:7— 13 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverGeneratedReaderWriterTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverMessageTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverMessageTests.cs:7— 2 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverMessageTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverMonoBehaviourTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverMonoBehaviourTests.cs:7— 5 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverMonoBehaviourTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests.cs:7— 28 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverNetworkBehaviourTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverSyncDictionaryTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverSyncDictionaryTests.cs:8— 2 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverSyncDictionaryTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverSyncListTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverSyncListTests.cs:7— 6 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverSyncListTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
Tests excluded from compilation: Assets/Mirror/Tests/Editor/Weaver/WeaverTargetRpcTests.cs Assets/Mirror/Tests/Editor/Weaver/WeaverTargetRpcTests.cs:7— 4 test method(s) in this file, but `.WeaverTests.csproj` drops it from the build with `<Compile Remove="WeaverTargetRpcTests.cs" />`. It never compiles, never runs, and never reports as skipped — unlike a `Skip = "..."` attribute, nothing in a green test run says these tests are gone. Restore it to the compilation, or delete the file so the tree stops implying coverage that isn't there.
WriteOnlyPrivateField Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests~/NetworkBehaviourClientRpcParamAbstract.cs:9— private int monkeys — assigned 1 time(s), read never — this field is written and never read anywhere its type can be reached from, so the state it keeps answers no question: every assignment to it computes a value that nothing observes, on every instance, for the lifetime of each one. It reads as a flag the code branches on, and nothing branches on it. Delete the field and its assignments — or, if the value was MEANT to be consulted, the missing read is the defect this row is pointing at, and the branch that should have depended on it is not there.
WriteOnlyPrivateField Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests~/NetworkBehaviourClientRpcParamComponent.cs:9— private int monkeys — assigned 1 time(s), read never — this field is written and never read anywhere its type can be reached from, so the state it keeps answers no question: every assignment to it computes a value that nothing observes, on every instance, for the lifetime of each one. It reads as a flag the code branches on, and nothing branches on it. Delete the field and its assignments — or, if the value was MEANT to be consulted, the missing read is the defect this row is pointing at, and the branch that should have depended on it is not there.
WriteOnlyPrivateField Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests~/NetworkBehaviourCmdParamAbstract.cs:9— private int monkeys — assigned 1 time(s), read never — this field is written and never read anywhere its type can be reached from, so the state it keeps answers no question: every assignment to it computes a value that nothing observes, on every instance, for the lifetime of each one. It reads as a flag the code branches on, and nothing branches on it. Delete the field and its assignments — or, if the value was MEANT to be consulted, the missing read is the defect this row is pointing at, and the branch that should have depended on it is not there.
WriteOnlyPrivateField Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests~/NetworkBehaviourCmdParamComponent.cs:9— private int monkeys — assigned 1 time(s), read never — this field is written and never read anywhere its type can be reached from, so the state it keeps answers no question: every assignment to it computes a value that nothing observes, on every instance, for the lifetime of each one. It reads as a flag the code branches on, and nothing branches on it. Delete the field and its assignments — or, if the value was MEANT to be consulted, the missing read is the defect this row is pointing at, and the branch that should have depended on it is not there.
WriteOnlyPrivateField Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests~/NetworkBehaviourTargetRpcParamAbstract.cs:9— private int monkeys — assigned 1 time(s), read never — this field is written and never read anywhere its type can be reached from, so the state it keeps answers no question: every assignment to it computes a value that nothing observes, on every instance, for the lifetime of each one. It reads as a flag the code branches on, and nothing branches on it. Delete the field and its assignments — or, if the value was MEANT to be consulted, the missing read is the defect this row is pointing at, and the branch that should have depended on it is not there.
WriteOnlyPrivateField Assets/Mirror/Tests/Editor/Weaver/WeaverNetworkBehaviourTests~/NetworkBehaviourTargetRpcParamComponent.cs:9— private int monkeys — assigned 1 time(s), read never — this field is written and never read anywhere its type can be reached from, so the state it keeps answers no question: every assignment to it computes a value that nothing observes, on every instance, for the lifetime of each one. It reads as a flag the code branches on, and nothing branches on it. Delete the field and its assignments — or, if the value was MEANT to be consulted, the missing read is the defect this row is pointing at, and the branch that should have depended on it is not there.
Secret: gcp-api-key Assets/Mirror/Examples/Cloud/TanksWithListServer/Prefabs/TanksNetworkManager.prefab @ 7d0e907:50 (committed 2020-06-25)— gitleaks matched rule 'gcp-api-key' here, in git history. Deleting the file does not remove it — the commit persists on every clone, fork and backup — so the act is to ROTATE this credential and treat it as compromised. The "Rotate the exposed credentials" row carries the full procedure, including when a `.gitattributes` provenance declaration is the correct answer instead.
Secret: gcp-api-key Assets/Mirror/Examples/PongWithListServer/Prefabs/NetworkManager.prefab @ f1fdc95:50 (committed 2020-06-23)— gitleaks matched rule 'gcp-api-key' here, in git history. Deleting the file does not remove it — the commit persists on every clone, fork and backup — so the act is to ROTATE this credential and treat it as compromised. The "Rotate the exposed credentials" row carries the full procedure, including when a `.gitattributes` provenance declaration is the correct answer instead.
Secret: generic-api-key ProjectSettings/ProjectSettings.asset @ 5b187fe:506 (committed 2019-01-15)— gitleaks matched rule 'generic-api-key' here, in git history. Deleting the file does not remove it — the commit persists on every clone, fork and backup — so the act is to ROTATE this credential and treat it as compromised. The "Rotate the exposed credentials" row carries the full procedure, including when a `.gitattributes` provenance declaration is the correct answer instead.
Secret: generic-api-key ProjectSettings/ProjectSettings.asset @ e7fed2d:528 (committed 2018-12-02)— gitleaks matched rule 'generic-api-key' here, in git history. Deleting the file does not remove it — the commit persists on every clone, fork and backup — so the act is to ROTATE this credential and treat it as compromised. The "Rotate the exposed credentials" row carries the full procedure, including when a `.gitattributes` provenance declaration is the correct answer instead.
High IaC: DS-0002 Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile— Image user should not be 'root'. Specify at least 1 USER command in Dockerfile with non-root user as argument. Add 'USER <non root user name>' line to the Dockerfile. A container that starts as root runs your process with root's capabilities inside the namespace, so a compromise of the process starts from there. The step: create an unprivileged account in the image (`RUN adduser --system --no-create-home app`), give it ownership of the paths the process writes at runtime (`COPY --chown=` on those layers, or a `RUN chown -R`), and end the final stage with `USER app` so it is the default at start. Build stages that only compile can stay root; it is the stage that RUNS that needs the account. If the process genuinely requires root — it manages the container runtime, ptraces another process or opens raw devices — say so here rather than making a change that breaks it.
High IaC: DS-0029 Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile:12— 'apt-get' missing '--no-install-recommends'. '--no-install-recommends' flag is missed: 'apt-get update && apt-get install -y ca-certificates && apt-get clean && update-ca-certificates'. Add '--no-install-recommends' flag to 'apt-get'.
Leaked secret: signing-key Assets/Mirror/Hosting/Edgegap/Models/SDK/AppVersionEnv.cs:49— signing-key detected. Treat the value as compromised: it is readable by everyone who has ever had the repository, and deleting the line does not un-publish it. In order — (1) REVOKE it at whatever issued it and issue a replacement, which is the only step that actually closes the exposure; (2) load the replacement at run time from your platform's secret store or the process environment instead of from the tree, so no future value is committable; (3) remove the file or line and add its path to the repository's ignore rules, so it cannot come back; (4) if the value was ever live, purge it from the history as well, since a clone taken before the deletion still carries it. If this is instead a FIXTURE — key material generated for tests and valid nowhere — then the exposure is nil and the fix is to make that legible: generate it in test setup, or keep it under a test-data path, so a reader (and this scan) can tell it from the real thing.
NoWarnInCsproj Assets/Mirror/Tests/Editor/Weaver/.WeaverTests.csproj:16— 0169 — this warning is switched off for the WHOLE project, in every file it builds, including code written years from now: nothing at the call site records that the rule was ever silenced, so the next reader has no reason to look here. Fix what the rule is reporting and drop the code from the list, or — if some occurrences really are legitimate — narrow the suppression to those sites and give each one its reason, so the rule keeps protecting the rest of the project.
Orphaned knowledge Assets/Mirror/Transports/Encryption/EncryptedConnection.cs— No living knowledge remains for this large file — its last meaningful change has decayed away; if it breaks, no one currently understands it. Schedule a read-through / add characterisation tests before it bites.
Change coupling: KcpClient.cs ↔ KcpServer.cs Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs— `Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs` and `Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs` change together 77% of the time (20 of the 26 commits that touched whichever of the two files changed less often, counting a file under its earlier names as well — a repo-wide or module-wide sweep is evidence about the sweep rather than about any pair inside it and is left out of BOTH sides of this ratio, while a dependency bump or a formatter/rename sweep is left out of the shared count ONLY, so the two sides are not taken over identical commit sets). They sit in the same directory, and in this ecosystem sibling files there normally share one namespace/package — so a direct reference between them needs no import and this pass cannot see whether one exists. Read the pair before acting: if one file only DECLARES what the other consumes (a constants/types file beside its user), the co-change is definitional and the question is whether the split earns its keep; if they duplicate structure, extract the common part into a shared function or type they both call; if neither holds, the coupling is hidden and worth breaking. You can check this without leaving the row: of the 20 shared commits counted here, the most recent 3 are `809f7a4a` feature: kcp2k exposes LocalEndPoint (credits: Whale); `ee957f3f` fix: kcp2k V1.35. secure cookie to prevent UDP spoofing. fixes: #3286…; `1a34614e` KcpClient/Server: change config visibility to protected — run `git show` on any of them.
Change coupling: NetworkReaderPool.cs ↔ NetworkWriterPool.cs Assets/Mirror/Core/NetworkReaderPool.cs— `Assets/Mirror/Core/NetworkReaderPool.cs` and `Assets/Mirror/Core/NetworkWriterPool.cs` change together 75% of the time (15 of the 20 commits that touched whichever of the two files changed less often, counting a file under its earlier names as well — a repo-wide or module-wide sweep is evidence about the sweep rather than about any pair inside it and is left out of BOTH sides of this ratio, while a dependency bump or a formatter/rename sweep is left out of the shared count ONLY, so the two sides are not taken over identical commit sets). They sit in the same directory, and in this ecosystem sibling files there normally share one namespace/package — so a direct reference between them needs no import and this pass cannot see whether one exists. Read the pair before acting: if one file only DECLARES what the other consumes (a constants/types file beside its user), the co-change is definitional and the question is whether the split earns its keep; if they duplicate structure, extract the common part into a shared function or type they both call; if neither holds, the coupling is hidden and worth breaking. You can check this without leaving the row: of the 15 shared commits counted here, the most recent 3 are `3d6233b9` NetworkReader/WriterPool: expose Count for testing; `fcd3a6b9` perf: remove some unnecessary inlining to optimize instruction cache; `24142b08` comments (at that commit the files were still `Assets/Mirror/Runtime/NetworkReaderPool.cs` and `Assets/Mirror/Runtime/NetworkWriterPool.cs`) — run `git show` on any of them.
Change coupling: Batcher.cs ↔ NetworkClient.cs Assets/Mirror/Core/Batching/Batcher.cs— `Assets/Mirror/Core/Batching/Batcher.cs` and `Assets/Mirror/Core/NetworkClient.cs` change together 50% of the time (5 of the 10 commits that touched whichever of the two files changed less often, counting a file under its earlier names as well — a repo-wide or module-wide sweep is evidence about the sweep rather than about any pair inside it and is left out of BOTH sides of this ratio, while a dependency bump or a formatter/rename sweep is left out of the shared count ONLY, so the two sides are not taken over identical commit sets). They sit in different directories, but in this ecosystem the namespace is declared in the FILE, not by the folder — so the two may well share one namespace and reference each other with no import for this pass to see. Read the pair before acting: if one derives from or overrides the other, the dependency is explicit in the type declaration and the co-change is definitional; if one registers itself into the other through a hook or an initialiser, the missing dependency is DELIBERATE and the thing to add is a comment saying so; if they simply belong together, co-locate them; if none of these holds, the coupling is hidden and worth breaking. You can check this without leaving the row: of the 5 shared commits counted here, the most recent 3 are `e35be5fb` fix(NetworkConnection): batches are now properly returned to NetworkW…; `13fd7211` fix: Batching VarInt size header added to fix custom serializated Net…; `0402d31e` breaking: PooledNetworkReader/Writer renamed to NetworkReader/WriterP… (at that commit the files were still `Assets/Mirror/Runtime/Batching/Batcher.cs` and `Assets/Mirror/Runtime/NetworkClient.cs`) — run `git show` on any of them.
Change coupling: Batcher.cs ↔ NetworkServer.cs Assets/Mirror/Core/Batching/Batcher.cs— `Assets/Mirror/Core/Batching/Batcher.cs` and `Assets/Mirror/Core/NetworkServer.cs` change together 50% of the time (5 of the 10 commits that touched whichever of the two files changed less often, counting a file under its earlier names as well — a repo-wide or module-wide sweep is evidence about the sweep rather than about any pair inside it and is left out of BOTH sides of this ratio, while a dependency bump or a formatter/rename sweep is left out of the shared count ONLY, so the two sides are not taken over identical commit sets). They sit in different directories, but in this ecosystem the namespace is declared in the FILE, not by the folder — so the two may well share one namespace and reference each other with no import for this pass to see. Read the pair before acting: if one derives from or overrides the other, the dependency is explicit in the type declaration and the co-change is definitional; if one registers itself into the other through a hook or an initialiser, the missing dependency is DELIBERATE and the thing to add is a comment saying so; if they simply belong together, co-locate them; if none of these holds, the coupling is hidden and worth breaking. You can check this without leaving the row: of the 5 shared commits counted here, the most recent 3 are `e35be5fb` fix(NetworkConnection): batches are now properly returned to NetworkW…; `13fd7211` fix: Batching VarInt size header added to fix custom serializated Net…; `0402d31e` breaking: PooledNetworkReader/Writer renamed to NetworkReader/WriterP… (at that commit the files were still `Assets/Mirror/Runtime/Batching/Batcher.cs` and `Assets/Mirror/Runtime/NetworkServer.cs`) — run `git show` on any of them.
Medium IaC: WD-DOCKER-0003 Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile:1— Line 1 builds this stage `FROM ubuntu:22.04` — a tag, which is a POINTER the image's publisher can move at any time, not the image you tested against. The tag is re-pushed for a rebuild, a backport or a compromise alike, so the same commit produces a different image on a different day and a change nobody here made arrives with no diff to review; a build that reproduces is also the only way to say what is actually running when an advisory lands. Pin the reference to its DIGEST, which is the image's content address and cannot be repointed — `FROM ubuntu:22.04@sha256:…` — resolve today's with `docker buildx imagetools inspect ubuntu:22.04 --format '{{.Manifest.Digest}}'` (or `docker pull ubuntu:22.04 && docker inspect --format '{{index .RepoDigests 0}}' ubuntu:22.04`). Keep the tag in front of the digest as documentation: it tells a reader which release this is while the digest decides what is pulled. Bumping it then becomes a deliberate commit a reader — or Dependabot/Renovate, both of which update digest pins — can review, instead of a silent change at the next build. This applies to a build-only stage too: the stage that compiles decides what ends up in the layers you ship.
Medium IaC: CKV_DOCKER_2 Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile.meta:1— Ensure that HEALTHCHECK instructions have been added to container images
Medium IaC: CKV_DOCKER_3 Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile.meta:1— Ensure that a user for the container has been created
No assertions: AddMessage Assets/Mirror/Tests/Editor/Batching/BatcherTests.cs:52— This method's body runs code, and no assertion call was recognised in it. Recognised by name: Assert*, *Should*/ShouldBe*, Verify, Expect, Throws, Record, Received/DidNotReceive, MustHaveHappened/MustNotHaveHappened, EnsureSuccessStatusCode and *AndEnsure* — so verification routed through a helper of your own naming, through a base-class or callback object whose members hold the assertions, or through a harness that fails by throwing under some other name, is not visible to this check and is not counted here. Read it as 'no assertion this check knows how to see', and if that is right, add one.
No assertions: RegisterThread_AddsThreadId Assets/Mirror/Tests/Editor/Threading/ThreadLogTests.cs:22— This method's body runs code, and no assertion call was recognised in it. Recognised by name: Assert*, *Should*/ShouldBe*, Verify, Expect, Throws, Record, Received/DidNotReceive, MustHaveHappened/MustNotHaveHappened, EnsureSuccessStatusCode and *AndEnsure* — so verification routed through a helper of your own naming, through a base-class or callback object whose members hold the assertions, or through a harness that fails by throwing under some other name, is not visible to this check and is not counted here. Read it as 'no assertion this check knows how to see', and if that is right, add one.
Medium: watchdog-xmldocument-dtd-entity-expansion-csharp Assets/Mirror/Editor/AndroidManifestHelper.cs:30— Loading this `XmlDocument` hands the document to a reader that opens with DTD processing enabled. `LoadXml`, and the `Load` overloads that take a path, a `Stream` or a `TextReader`, build that reader inside the call; an `XmlTextReader` handed in already starts at `DtdProcessing.Parse` and reaches the same place. It is the opposite of the `XmlReaderSettings` default of `DtdProcessing.Prohibit` that `XmlReader.Create` starts from, so the loading path is the whole difference here: a document `XmlReader.Create` rejects outright is expanded on this line instead. An internal DTD subset may declare entities that refer to each other, and every level multiplies — ten entities each referring ten times to the level below reaches a million expansions in six short lines, and all of it expands in memory before this code sees a single element. Measured on .NET 10.0.10 through this exact shape, a 478-byte document produced 800,000 characters, and a 652-byte one ran until the runtime stopped it at its 10,000,000-character entity ceiling — so roughly 20 MB of memory out of a request the sender can repeat, which bounds the damage per document rather than removing it. Where the XML arrives from somewhere this program does not control — a downloaded manifest, an uploaded file, a package supplied by a third party — that is a document its sender can use to spend this process's memory and CPU. Note that `XmlResolver = null` answers a different question: it governs retrieval of EXTERNAL entities, which this runtime already refuses by default, and probed in exactly that spelling the internal subset still expanded to the same 800,000 characters. Because `XmlDocument` has no DTD switch of its own, the fix is to choose the loading path rather than to set a property on the document: build the reader with `XmlReader.Create`, whose `XmlReaderSettings` already default to `DtdProcessing.Prohibit`, and hand it to `Load(reader)`. Where a DTD genuinely has to be honoured, pair `DtdProcessing = DtdProcessing.Parse` with a `MaxCharactersFromEntities` bound — probed at 10,000, that same document is refused rather than expanded.
Change coupling clique: CommandProcessor.cs, RpcProcessor.cs, TargetRpcProcessor.cs Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs— 3 files — `Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs`, `Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs`, `Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs` — all change together with no explicit dependency: a fully-connected co-change clique, not 3 separate couplings. They share one concern (thin parallel siblings over a common abstraction), so extract the shared part into ONE unit and the whole clique's coupling clears at once — you do not need to break each pair individually.
Unpinned build actions — CI references GitHub Actions by a floating ref (@main / @tag) rather than a pinned commit SHA, weakening build integrity. 10 floating ref(s) across 4 workflow file(s), 1 of them mutable BRANCH refs — pin those first. Each floating ref is itemized at file:line by the SAST (D29) lens.
D36 · Supply-chain Provenance & Signing· PR-triggered workflow without a permissions block · ×1
PR-triggered workflow without a permissions block — 1 workflow(s) triggered by pull_request run at least one job at the repository's default GITHUB_TOKEN scope (main.yml) — no top-level `permissions:` block, and at least one job declaring none of its own — while 1 sibling workflow in the same repository is already scoped. A job that declares nothing inherits the workflow-level block, and with no workflow-level block it inherits the repository default, so scoping some of a workflow's jobs leaves the rest exactly as wide as scoping none. Pull-request runs build the least-trusted code in the repository; give each of these workflows its own least-privilege block — `permissions: {contents: read}` at the top of the workflow, widened per job only where a job genuinely writes.
D36 · Supply-chain Provenance & Signing· Workflow holding a long-lived secret is unscoped · ×1
Workflow holding a long-lived secret is unscoped — 2 workflow(s) inject a long-lived repository secret while leaving at least one job at the repository's default GITHUB_TOKEN scope (RunUnityTests.yml → secrets.UNITY_LICENSE, secrets.UNITY_PASSWORD, secrets.CODECOV_TOKEN, SonarQube.yml → secrets.SONAR_TOKEN) — no top-level `permissions:` block, and at least one job declaring none of its own — so that job carries the default token on top of the credential it already holds. A compromised or repointed action in one of these jobs inherits both — the long-lived secret AND write access to code, releases and packages. Sibling workflows in this repository already declare `permissions:`, so the repository-wide check does not flag it. Give each of these its own least-privilege block — `permissions: {contents: read}` at the top, widened per job only where a job genuinely writes.
Coverage not measured — no coverage collector is wired up — Coverage NOT MEASURED: `--collect:"XPlat Code Coverage"` names a data collector that ships in the `coverlet.collector` package, and this repository wires up none — no test project references it and no runsettings declares one. The absence of coverage here is therefore not evidence about the suite or about our analyzer environment: without a collector, `--collect` produces nothing even from a suite that builds and passes. Add a `coverlet.collector` PackageReference to the test project(s) (or commit the Cobertura/OpenCover/lcov report your CI produces) and real coverage will be measured. It is excluded from the score rather than counted as a near-zero defect.
SC1 · Supply-chain hygiene· JavaScript dependencies are not locked · ×1
JavaScript dependencies are not locked — No package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lock — JS installs aren't reproducible (SSDF PW.4.4). Commit your package manager's lockfile and install from it (`npm ci`, `yarn --immutable`, `pnpm i --frozen-lockfile` or `bun i --frozen-lockfile`). Advisory — never scored.
redundant comment Assets/Mirror/Tests/Editor/Weaver/WeaverClientRpcTests~/OverrideAbstractClientRpc.cs:11— "do something" — delete - 'do something' is literally what the method does
Off-boarding risk: anonymized user #1 — If anonymized user #1 becomes unavailable, 12 significant file(s) lose their only recent owner: Assets/Mirror/Core/SyncDictionary.cs, Assets/Mirror/Core/SyncSet.cs, Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs, Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs, Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs, Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs, Assets/Mirror/Authenticators/UniqueNameAuthenticator.cs, Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs (+4 more). Pair on, review, or document these before any departure.
D16 · Bus Factor· Further sole-owners (lower concentration) · ×1
Further sole-owners (lower concentration) — 1 other contributor(s) are each the sole owner of a small amount of code below the off-boarding threshold — folded into the bus-factor score and metrics (15 single-owned of 217 analysed files in total, counted over production source files of roughly 2,400 bytes or more, excluding vendored, generated and example/demo trees and test files identified by path convention, largest first; 217 of the 410 production source files in this repository met that bar). They are anonymized user #2 (2 file(s)) — spread or document their files in the same way, at lower priority than the named off-boarding risks above.
Documentation: no installation or build instructions Assets/Mirror/Readme.txt— The README states requirements and gives download links but omits any build or setup instructions for installing Mirror into a Unity project. Add installation steps: open the Asset Store, search for Mirror, install it; add Mirror to the Project Settings->Player->Other Settings as a dependency.
D21 · Naming Consistency· Typo in namespace/type name · ×1
Typo in namespace/type name: 'Virutal' is used instead of the standard spelling 'Virtual'. This appears in both the Client and Server attribute test namespaces/types. — Rename to 'ClientAttributeOnVirtualMethod' and 'ServerAttributeOnVirtualMethod' to correct the spelling error. (symbols: WeaverClientServerAttributeTests.ClientAttributeOnVirutalMethod, WeaverClientServerAttributeTests.ServerAttributeOnVirutalMethod)
D28 · Secrets (history)· Rotate the exposed credentials · ×1
Rotate the exposed credentials — git history can't be un-committed — Some of these secrets are in git HISTORY: deleting the file does not remove them (the commit persists on every clone, fork and backup). The remediation is to ROTATE each historically-exposed credential and treat it as compromised — not to delete the file. Rewriting history is disruptive and unreliable across existing forks. (Working-tree-only secrets — no commit — can instead be removed from the file and moved to a secret store.) If flagged rows sit in content that is not this repository's own authored code — tool-emitted artifacts, recorded scanner output, wholesale-vendored third-party files — declare that provenance in .gitattributes (`path/** linguist-generated` or `linguist-vendored`): this scanner honors the repository's own declarations and stops counting such content, history-anchored rows included, on the next scan. The declaration is a reviewable statement about provenance, not a suppression — never use it on files that carry your own credentials. These 4 location(s) do not all need the same action: 2 sit inside a test/fixture/sample tree and 2 do not. Rotate the ones outside those trees as stated above. For the fixture ones there may be no live credential to revoke — confirm each value was never reused outside the tests (a fixture key shared with a staging or demo environment IS a live credential and must be rotated), then generate that material at test time instead of committing it, and record the deliberate exposure where a reader of the file will see it.
D30 · Dependency Vulnerabilities· Scanner failed to run · ×1
Scanner failed to run — not a clean result — osv: the scanner produced no output at all, so no dependency was actually scanned. This is reported as a measurement gap rather than a clean pass. osv-scanner documents exit 128 as "no package sources found" — but this scan only invoked it because the tree declares a source it supports (`pom.xml` and the resolved lockfiles among them), so the tool's report and this repository disagree. Check that those sources are reachable from the directory the scan ran in before concluding anything is missing. If this tree really does commit only range-pinned manifests, commit the lockfile your package manager produces — but only where your ecosystem HAS one and commits it: Maven has none at all (osv-scanner reads `pom.xml` directly) and Gradle writes one only under opt-in dependency locking, while a published Composer package and a published Dart library both deliberately exclude theirs. In those three, generate one in CI before the scan instead — a resolved dependency list or an SBOM fed to the scanner.
D34 · Knowledge Freshness· Further orphaned files (smaller) · ×1
Further orphaned files (smaller) — 16 smaller file(s) also have no living knowledge — folded into the freshness score and metrics rather than raised one row each — most significant first: Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs, Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs, Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs, Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs, Assets/Mirror/Components/Profiling/BaseUIGraph.cs, Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs, Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs, Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs (and 8 more) (17 orphaned of 217 analysed files in total, counted over production source files of roughly 2,400 bytes or more, excluding vendored, generated and example/demo trees and test files identified by path convention, largest first; 217 of the 410 production source files in this repository met that bar). Attach the read to the next change that touches one of them: have a second person review that change, and leave behind a short comment or test recording what the file is for, so the knowledge comes back at the cost of a change you were making anyway.
No build provenance — No SLSA provenance generation or build attestation found in CI — nothing binds a released artifact to the build that produced it, so a consumer cannot tell your artifact from a substituted one. On GitHub Actions, `actions/attest-build-provenance` (or slsa-github-generator) emits one from the job's own OIDC identity; elsewhere, run `cosign attest` over the released artifact from the release pipeline and publish the attestation beside it. What was searched, so you can tell an absence from a miss: the 5 CI config file(s) in this repository, the scripts they invoke, and this repository's own composite actions — matched against the provenance acts by name (`attest-build-provenance`, `actions/attest`, `slsa-github-generator`, `cosign attest`, `in-toto`, `--provenance`) plus npm/PyPI trusted publishing. An attestation produced by a tool none of those names, or by a pipeline whose configuration is not in this repository, is not seen by this check.
No artifact signing — No artifact signing found in CI — sign your released artifacts with whatever your ecosystem ships (a GPG/minisign detached signature — or `cosign sign-blob` — over the release archives, or over a checksum file published alongside them) so consumers can verify what you built. What was searched, so you can tell an absence from a miss: this repository's CI files, the scripts they invoke and its own composite actions, its root build manifest and its release manifest, its release task runner if it has one, and its own release automation script — plus the trusted-publishing declarations that sign on the registry's side. A signature produced by a pipeline whose configuration is not in this repository, or by a signer whose name is none of the ones this check carries, is not seen here.
D36 · Supply-chain Provenance & Signing· No SBOM · ×1
No SBOM — No SBOM generation or committed SBOM found — produce one with what your ecosystem ships (`syft` (or `anchore/sbom-action` in CI) over the source tree or released image). Publish it as a release asset (`*.spdx.json` / `*.cdx.json`) so consumers can see what they are installing. What was searched, so you can tell an absence from a miss: this repository's CI files, the scripts they invoke and its own composite actions, any SBOM committed to the repository, the SBOM configuration declared in its build and release manifests, and the container-build SBOM inputs. An SBOM produced by a pipeline whose configuration is not in this repository, or by a generator whose name is none of the ones this check carries, is not seen here.
No dependency advisory monitoring — Nothing in this repository re-checks its pinned dependencies when an advisory is published: no Dependabot or Renovate configuration (`.github/dependabot.yml`, `renovate.json`), no advisory scanner invoked from CI and no committed scanner configuration (`deny.toml`, `.cargo/audit.toml`, `osv-scanner.toml`, `.snyk`). The manifests present — `npm` (package.json), `nuget` (*.csproj) — pin versions that advisories are published against on the advisory database's clock, not this repository's, so every advisory that lands against a pinned version stays unseen until someone points a scanner at the tree (any already published are reported under D30, one row per package — this row names the control whose absence lets them accumulate). GitHub's Dependabot alerts, if switched on in the repository settings, notify but leave nothing in the tree; a committed configuration is what makes the control reviewable and carries it to a fork or mirror. Add a `.github/dependabot.yml` with one `package-ecosystem` entry per manifest listed (or a `renovate.json`) so each advisory arrives as a pull request against the affected lockfile; or wire the ecosystem's own advisory scanner into CI on a `schedule:` trigger — npm: `npm audit` / `pnpm audit` / `yarn npm audit`; nuget: `dotnet list package --vulnerable` failing on output, or promote the SDK's audit warnings to errors (`<WarningsAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsAsErrors>`).
No ADRs — No Architecture Decision Records found — no conventional ADR directory, no numbered `NNNN-title` documents in any markup this check reads, and nothing ADR-shaped by content. Design rationale recorded elsewhere (a design-notes tree, a mailing list, pull-request discussion) is not visible to this check and is not re-findable per decision, so a future maintainer cannot ask why one choice was made and get an answer.
No src/ separation — Production code isn't grouped under a src/ folder, so nothing in the layout marks which files are the product and which are its tooling.
P12 · CI test-gate honesty· Sleep-based test synchronization · ×1
Sleep-based test synchronization — 15 Task.Delay/Thread.Sleep call(s) in test code synchronize with background work by sleeping — a known flakiness precursor on slow runners. Prefer polling with a deadline or completion signals.
Skipped (documented): SendCommandInternal Assets/Mirror/Tests/Editor/NetworkBehaviour/NetworkBehaviourTests.cs:145— Skipped with a documented reason — a deferral, not lazy debt: NetworkServerTest.SendCommand does it already
Skipped (documented): SendRPCInternal Assets/Mirror/Tests/Editor/NetworkBehaviour/NetworkBehaviourTests.cs:148— Skipped with a documented reason — a deferral, not lazy debt: ClientRpcTest.cs tests Rpcs already
Skipped (documented): SendTargetRPCInternal Assets/Mirror/Tests/Editor/NetworkBehaviour/NetworkBehaviourTests.cs:151— Skipped with a documented reason — a deferral, not lazy debt: TargetRpcTest.cs tests TargetRpcs already
Skipped (documented): Send Assets/Mirror/Tests/Editor/NetworkClient/NetworkClientTests.cs:54— Skipped with a documented reason — a deferral, not lazy debt: NetworkServerTest.SendClientToServerMessage does it already
Skipped (documented): HandleCommand Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs:652— Skipped with a documented reason — a deferral, not lazy debt: NetworkServerTest.SendCommand does it already
Skipped (documented): HandleRpc Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs:655— Skipped with a documented reason — a deferral, not lazy debt: RpcTests do it already
Skipped (documented): WriterShouldIncludeFieldsInBaseClass Assets/Mirror/Tests/Editor/NetworkReaderWriter/FieldsInBaseClasses.cs:37— Skipped with a documented reason — a deferral, not lazy debt: Destroy is needed for the code. Can't be called in Edit mode.
Skipped (documented): OnClientToServerSync_WithClientAuthority_Nullables_Uses_Last Assets/Mirror/Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs:233— Skipped with a documented reason — a deferral, not lazy debt: Nullables not supported
Skipped (documented): OnServerToClientSync_WithClientAuthority_Nullables_Uses_Last Assets/Mirror/Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs:311— Skipped with a documented reason — a deferral, not lazy debt: Nullables not supported
Skipped (documented): Insert_ComputesAverageDeliveryInterval_Scrambled Assets/Mirror/Tests/Editor/SnapshotInterpolation/SnapshotInterpolationTests.cs:343— Skipped with a documented reason — a deferral, not lazy debt: Delivery Time EMA doesn't handle scrambled packages differently yet
Skipped (documented): LargestAbsoluteComponentIndexBenchmark Assets/Mirror/Tests/Editor/Tools/CompressionTests.cs:204— Skipped with a documented reason — a deferral, not lazy debt: Enable when needed.
Skipped (documented): GivesWarningWhenRegisteringExistingExtensionMethod Assets/Mirror/Tests/Editor/Weaver/WeaverGeneratedReaderWriterTests.cs:122— Skipped with a documented reason — a deferral, not lazy debt: Enable again when we don't have obsoletes in NetworkWriter anymore.
Appendix B — Reproduction & audit trail
Every external tool invocation behind a deep-scan dimension — the tool, its captured version, the exact command, how many findings it yielded, and a link to the retained raw output. To reproduce any finding: check out the same commit and run the command shown (repo-relative — never an absolute scratch path). The complete raw scanner output is retained verbatim under artifacts/raw/ (indexed in artifacts/raw/index.json); per-invocation exit codes and wall-clock durations are in sidecar.json — kept out of this table so the rendered report stays byte-identical across runs of the same commit.
semgrep: not applicable — No personal data was found crossing a boundary the PII/GDPR ruleset checks — nothing written to a log or console sink, placed in a URL or query string, or persisted to browser storage. That is a clean result for the LEAK surface only: this ruleset detects personal data escaping, it does not inventory the personal data a repository holds, so it is not evidence that this repository has no personal-data surface. The personal-data map (Appendix C) and the C1-C5 compliance cards are what speak to that. semgrep could not parse 3 file(s) — `Assets/Mirror/Core/Tools/Utils.cs`, `Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs`, `Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs` — so the PII/GDPR sweep did not cover the unparsed regions of them; rows reported elsewhere in those files are real.
runtime-hardening: not applicable — No Kubernetes/orchestration workloads found in the repository manifests; network egress policy is a cluster-native control that may live at the platform/firewall layer, so there is nothing to assess here.
runtime-hardening: not applicable — No Kubernetes/orchestration workloads found in the repository manifests; seccomp/AppArmor/SELinux confinement is a workload-level control, so there is nothing to assess here.
runtime-hardening: not applicable — No Kubernetes/orchestration workloads found in the repository manifests; runtime threat-detection and admission-control policy are cluster-level controls, so there is nothing to assess here.
0
—
Run 01a08d94-1d03-770d-bf21-f04ccfc949b7 · every finding is also locatable in findings.md, and the complete scoring record (with exit codes + durations) in sidecar.json.
Issues: 50 · Warnings: 16 · Recommendations: 16 · Info: 12 — Appendix A · all findings · full markdown report.
Generated by Watchdog — deterministic code-health analysis. 10-09-2026 @ 23:08 UTC.
Downloadable artifacts
Machine-readable and reproducible from this commit + frozen rubric — drop them straight into a contract appendix, a CRA dossier, or a downstream SCA / VEX tool.