diff --git a/.editorconfig b/.editorconfig index 2f9d6e0..3b41992 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,39 +1,141 @@ -# AppKit code style. Enforced in every build via Directory.Build.props -# (EnforceCodeStyleInBuild + TreatWarningsAsErrors) and by the CI format gate -# (dotnet format --verify-no-changes --severity error). +# KofTwentyTwo .NET code style (standards: K22-CODE-CS): the Kingsrook code style +# (https://github.com/QRun-IO/qqq/blob/develop/CODE_STYLE.md) applied to C#. +# Copy to the repository root. Severities of "warning" fail the build because the +# companion Directory.Build.props sets TreatWarningsAsErrors and EnforceCodeStyleInBuild, +# so local `dotnet build` and CI enforce exactly the same rules. The resharper_* keys +# apply the Kingsrook blank-line rules in Rider and ReSharper, which .NET's own +# formatter cannot express. root = true [*] charset = utf-8 -end_of_line = crlf +end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space -indent_size = 4 +# Kingsrook: always 3 spaces, never tabs. +indent_size = 3 +tab_width = 3 -# MSBuild, config, and workflow files use 2-space indentation. -[*.{csproj,props,targets,slnx,xml,json,yml,yaml}] +# YAML stays at 2: the YAML ecosystem (GitHub Actions, Dependabot) assumes it. +[*.{yml,yaml}] indent_size = 2 +[*.md] +trim_trailing_whitespace = false + +# Windows-only script formats require CRLF. +[*.{cmd,bat}] +end_of_line = crlf + [*.cs] -# System usings first, as the codebase already does (used by IDE "Sort Usings"). +#### Formatting (IDE0055 makes every rule below a build error) #### +dotnet_diagnostic.IDE0055.severity = warning dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_space_after_cast = false +# Kingsrook: no space between a control keyword and its parenthesis: if(x), for(...), catch(...). +csharp_space_after_keywords_in_control_flow_statements = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_semicolon_in_for_statement = true +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true -# Braces on every control-flow block; the codebase is already 100% compliant. -csharp_prefer_braces = true:warning +#### Kingsrook blank lines and layout (Rider / ReSharper; .NET formatter cannot express) #### +resharper_blank_lines_around_method = 3 +resharper_blank_lines_around_property = 3 +resharper_blank_lines_around_auto_property = 1 +resharper_blank_lines_around_type = 3 +resharper_blank_lines_after_using_list = 2 +resharper_keep_blank_lines_in_code = 1 +resharper_keep_blank_lines_in_declarations = 3 +resharper_brace_style = next_line +resharper_indent_size = 3 +resharper_space_after_keywords_in_control_flow_statements = false +resharper_max_line_length = 999 +resharper_wrap_before_first_method_call = true +resharper_wrap_chained_method_calls = chop_if_long -# Every project uses file-scoped namespaces (AppKit has no XAML-generated code). +#### Usings and namespaces #### +# IDE0005 needs GenerateDocumentationFile=true to run during build (set in Directory.Build.props). +dotnet_diagnostic.IDE0005.severity = warning +csharp_using_directive_placement = outside_namespace:warning csharp_style_namespace_declarations = file_scoped:warning +dotnet_diagnostic.IDE0161.severity = warning -# CA1716 warns that IActivityLog.Error collides with a VB keyword; this is a C#-only -# codebase and Error is the natural name for a log level. -dotnet_diagnostic.CA1716.severity = none +#### var: only when the type is apparent from the right-hand side #### +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = false:warning +dotnet_diagnostic.IDE0007.severity = warning +dotnet_diagnostic.IDE0008.severity = warning -# CA1859 asks callers to replace IActivityLog/ISecretVault parameters with concrete types -# for micro-performance; those interfaces are the codebase's deliberate test seams. -dotnet_diagnostic.CA1859.severity = none +#### Braces, modifiers, readonly #### +csharp_prefer_braces = true:warning +dotnet_diagnostic.IDE0011.severity = warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_diagnostic.IDE0040.severity = warning +dotnet_style_readonly_field = true:warning +dotnet_diagnostic.IDE0044.severity = warning +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:warning +dotnet_diagnostic.IDE0036.severity = warning -# Naming: interfaces are I-prefixed; types and members are PascalCase. +#### this. qualification: never #### +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_diagnostic.IDE0003.severity = warning + +#### Language keywords over framework type names #### +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +dotnet_diagnostic.IDE0049.severity = warning + +#### Dead code and redundancy #### +dotnet_diagnostic.IDE0051.severity = warning # unused private member +dotnet_diagnostic.IDE0052.severity = warning # unread private member +dotnet_diagnostic.IDE0059.severity = warning # unnecessary assignment +dotnet_diagnostic.IDE0060.severity = warning # unused parameter +dotnet_diagnostic.IDE0035.severity = warning # unreachable code +dotnet_diagnostic.IDE0004.severity = warning # unnecessary cast +dotnet_diagnostic.IDE0100.severity = warning # unnecessary equality operator +dotnet_diagnostic.IDE0080.severity = warning # unnecessary suppression operator +dotnet_diagnostic.IDE0079.severity = warning # unnecessary suppression +dotnet_code_quality_unused_parameters = all + +#### Modern language features (suggestions: shown in IDE, not enforced) #### +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion + +#### Naming #### dotnet_naming_rule.interfaces_begin_with_i.symbols = interfaces dotnet_naming_rule.interfaces_begin_with_i.style = i_prefix_pascal_case dotnet_naming_rule.interfaces_begin_with_i.severity = warning @@ -41,20 +143,69 @@ dotnet_naming_symbols.interfaces.applicable_kinds = interface dotnet_naming_style.i_prefix_pascal_case.required_prefix = I dotnet_naming_style.i_prefix_pascal_case.capitalization = pascal_case +dotnet_naming_rule.type_parameters_begin_with_t.symbols = type_parameters +dotnet_naming_rule.type_parameters_begin_with_t.style = t_prefix_pascal_case +dotnet_naming_rule.type_parameters_begin_with_t.severity = warning +dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter +dotnet_naming_style.t_prefix_pascal_case.required_prefix = T +dotnet_naming_style.t_prefix_pascal_case.capitalization = pascal_case + dotnet_naming_rule.types_and_members_are_pascal_case.symbols = types_and_members dotnet_naming_rule.types_and_members_are_pascal_case.style = pascal_case dotnet_naming_rule.types_and_members_are_pascal_case.severity = warning -dotnet_naming_symbols.types_and_members.applicable_kinds = class, struct, enum, delegate, property, method, event +dotnet_naming_symbols.types_and_members.applicable_kinds = namespace, class, struct, enum, delegate, property, method, event dotnet_naming_style.pascal_case.capitalization = pascal_case -[tests/**.cs] -# CA1707: xunit's Method_Scenario_Expectation naming uses underscores by convention. +dotnet_naming_rule.private_fields_are_underscore_camel.symbols = private_fields +dotnet_naming_rule.private_fields_are_underscore_camel.style = underscore_camel_case +dotnet_naming_rule.private_fields_are_underscore_camel.severity = warning +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = +dotnet_naming_style.underscore_camel_case.required_prefix = _ +dotnet_naming_style.underscore_camel_case.capitalization = camel_case + +dotnet_naming_rule.private_static_fields_are_s_camel.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_are_s_camel.style = s_camel_case +dotnet_naming_rule.private_static_fields_are_s_camel.severity = warning +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static +dotnet_naming_style.s_camel_case.required_prefix = s_ +dotnet_naming_style.s_camel_case.capitalization = camel_case + +dotnet_naming_rule.constants_are_pascal_case.symbols = constants +dotnet_naming_rule.constants_are_pascal_case.style = pascal_case +dotnet_naming_rule.constants_are_pascal_case.severity = warning +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +#### Analyzer adjustments (each one justified) #### +# CA1716: flags members named like VB keywords (e.g. ILog.Error). KofTwentyTwo is C#-only. +dotnet_diagnostic.CA1716.severity = none +# MA0004 (ConfigureAwait): app code runs on UI threads where ConfigureAwait(false) is +# wrong by default; libraries opt back in with a [*.cs] override in their own folder. +dotnet_diagnostic.MA0004.severity = none +# MA0049 (type name matches namespace): conflicts with the AppName/AppName.Core layout. +dotnet_diagnostic.MA0049.severity = none + +[**/{Generated,obj}/**.cs] +generated_code = true + +[{tests,test}/**.cs] +# xUnit's Method_Scenario_Expectation names use underscores by convention. dotnet_diagnostic.CA1707.severity = none -# CA1861: inline array arguments keep test data at the assertion site; each test runs once, -# so the "repeated allocation" concern does not apply. +# Inline arrays keep test data at the assertion; each test runs once. dotnet_diagnostic.CA1861.severity = none +# Test projects are not documented API. +dotnet_diagnostic.CS1591.severity = none +# MA0048 (one type per file): a test file groups the test classes and fakes of one feature. +dotnet_diagnostic.MA0048.severity = none [samples/**.cs] # CA1822: XAML event handlers and x:Bind targets must be instance members; the XAML # compiler generates instance calls (this.Handler) for them. dotnet_diagnostic.CA1822.severity = none +# IDE0060: XAML event handlers must keep the (sender, e) signature the XAML compiler wires +# up, even when a handler uses neither; `dotnet format` cannot see that wiring. +dotnet_diagnostic.IDE0060.severity = none diff --git a/.gitattributes b/.gitattributes index b8c94f8..4018296 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,6 @@ -# Text files are stored LF and checked out CRLF on every machine, matching -# .editorconfig (end_of_line = crlf) so the dotnet format gate behaves the same -# at developers' desks and on CI runners regardless of their git config. -* text=auto eol=crlf +# Text is stored and checked out as LF on every OS (K22-CODE-07, K22-REPO-03). +* text=auto eol=lf +*.png binary +*.ico binary +*.cmd text eol=crlf +*.bat text eol=crlf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7b08b58..1cf56cb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ -# AppKit is maintained by a single owner; all changes route to them for review. +# Every path has an owner (K22-REPO-08). Add narrower rules below for areas that +# need a specific reviewer once the project has more than one maintainer. * @KofTwentyTwo diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b2ee8fb..35fe00a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,47 +1,40 @@ name: Bug report -description: Something in an AppKit package is broken +description: Something is broken labels: [bug] body: + - type: markdown + attributes: + value: >- + Security problem? Do not file it here; use the private + [vulnerability report](https://github.com/KofTwentyTwo/AppKit/security/advisories/new). - type: input id: version attributes: - label: AppKit version - placeholder: "e.g. 0.1.0" + label: Version + description: The exact version (Help, About, or `--version`). validations: required: true - - - type: dropdown - id: package - attributes: - label: Package - options: - - KofTwentyTwo.AppKit - - KofTwentyTwo.AppKit.Updates - - KofTwentyTwo.AppKit.WinUI - - KofTwentyTwo.AppKit.Wpf - validations: - required: true - - type: textarea id: what-happened attributes: label: What happened, and what did you expect? - description: Include any exact error message or stack trace. + description: Include exact error messages. validations: required: true - - type: textarea id: repro attributes: label: Steps to reproduce - description: Ideally against one of the sample apps in `samples/`. validations: required: true - - type: textarea id: environment attributes: label: Environment - description: Windows version (`winver`), packaged (MSIX) or unpackaged (Velopack), and the host app if relevant. - validations: - required: false + description: OS and version, and anything else relevant. + - type: textarea + id: logs + attributes: + label: Logs + description: Relevant log lines. Remove any tokens, passwords, or personal data first. + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358..36dc3c3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ -blank_issues_enabled: true +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://github.com/KofTwentyTwo/AppKit/security/advisories/new + about: Please report security issues privately, never as a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 6a1e2bc..213a1e3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,32 +1,20 @@ name: Feature request -description: Suggest something every KofTwentyTwo app should get from AppKit +description: Suggest an improvement labels: [enhancement] body: - type: textarea id: problem attributes: label: What problem would this solve? - description: Which apps need it, and what do they do today instead? validations: required: true - - type: textarea id: proposal attributes: - label: Proposed API or behavior + label: Proposed solution validations: required: true - - - type: dropdown - id: package + - type: textarea + id: alternatives attributes: - label: Which package? - options: - - KofTwentyTwo.AppKit - - KofTwentyTwo.AppKit.Updates - - KofTwentyTwo.AppKit.WinUI - - KofTwentyTwo.AppKit.Wpf - - New package - - Not sure - validations: - required: false + label: Alternatives considered diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8da14b7..ce95f83 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,23 @@ - + ## What - + ## Why - + + +## How it was tested + + ## Checklist -- [ ] `dotnet build AppKit.slnx -p:Platform=x64 -warnaserror` succeeds with **0 warnings** -- [ ] Unit tests pass and coverage of `KofTwentyTwo.AppKit` / `KofTwentyTwo.AppKit.Updates` is still **100%** -- [ ] UI changes have the same shape in WinUI **and** WPF, with a UI test in both samples -- [ ] `dotnet format AppKit.slnx --verify-no-changes --severity error` is clean -- [ ] Public API changes are intentional; breaking changes are called out for a major bump -- [ ] Docs updated (`README.md`, package READMEs) if behavior changed +- [ ] Commits are signed and signed off (`git commit -S -s`) +- [ ] Zero build warnings; format and lint are clean +- [ ] Tests added or updated; the coverage gate still passes +- [ ] Docs updated for user-visible changes +- [ ] No secrets, credentials, or personal data +- [ ] New dependencies are justified below, or none were added +- [ ] Substantial AI assistance, if any, is noted here diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1e51612..70ef038 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,35 +1,37 @@ -# Weekly dependency updates for NuGet packages and the GitHub Actions pinned in -# .github/workflows. Minor/patch bumps are grouped into a single PR to keep the -# noise down; major bumps still arrive as individual PRs for closer review. +# Dependabot for a KofTwentyTwo repository: copy to .github/dependabot.yml and keep +# the ecosystems the repository uses. Updates are grouped weekly. A 7-day cooldown +# lets the community catch a compromised release before it reaches us; security +# updates are not delayed by the cooldown. version: 2 updates: - - package-ecosystem: nuget - directory: "/" - # Updates land on dev like all other work and reach main via the gated PR flow. - target-branch: dev - schedule: - interval: weekly - day: monday - time: "06:00" - timezone: America/New_York - groups: - nuget-minor-patch: - applies-to: version-updates - patterns: - - "*" - update-types: - - minor - - patch - + # Always: keeps SHA-pinned actions and reusable workflows current (K22-CI-11). - package-ecosystem: github-actions - directory: "/" - target-branch: dev + directory: / schedule: interval: weekly day: monday - time: "06:00" - timezone: America/New_York + cooldown: + default-days: 7 groups: - actions-all: - patterns: - - "*" + actions: + patterns: ["*"] + commit-message: + prefix: ci + + # .NET: NuGet, including Directory.Packages.props and packages.lock.json. + - package-ecosystem: nuget + directory: / + schedule: + interval: weekly + day: monday + cooldown: + default-days: 7 + semver-major-days: 14 + groups: + minor-and-patch: + update-types: [minor, patch] + commit-message: + prefix: build + + # Other ecosystems (npm, pip/uv, cargo, gomod, swift, maven, gradle, terraform, + # docker): copy the block above and change package-ecosystem and directory. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60bd8de..5684dc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,101 +1,30 @@ -# Continuous integration: zero-warning build, unit tests with the 100% coverage gate, -# end-to-end UI tests of the sample apps, package creation, and the formatting gate. -name: CI +# .NET build, test, coverage, and format gates (K22-CI-01, K22-CI-02, K22-TEST-10). +# Produces the required contexts `ci / build-test`, `ci / format`, and `ci / ui-tests`. +# The coverage gate holds the two UI-framework-free packages at 100% line coverage. +name: ci on: - push: - branches: [main, dev] pull_request: + push: + branches: [main] -permissions: - contents: read +permissions: {} concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + group: ci-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - build-test: - name: Build and test (x64) - runs-on: windows-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up .NET SDK - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - # -warnaserror also fails on NuGetAudit warnings, so a known-vulnerable - # dependency breaks the build. - - name: Build solution (x64, warnings as errors) - run: dotnet build AppKit.slnx -p:Platform=x64 -warnaserror - - - name: Run unit tests with coverage - run: dotnet test tests/KofTwentyTwo.AppKit.Tests --no-build -p:Platform=x64 --settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory TestResults - - - name: Enforce 100% line coverage (AppKit, AppKit.Updates) - shell: pwsh - run: ./build/Assert-Coverage.ps1 -ResultsDirectory TestResults -Packages KofTwentyTwo.AppKit, KofTwentyTwo.AppKit.Updates - - - name: Upload coverage - if: always() - uses: actions/upload-artifact@v7 - with: - name: coverage-cobertura - path: TestResults/**/coverage.cobertura.xml - if-no-files-found: warn - - # Proves every package still packs (metadata, README, dependencies) on every PR, - # not only at release time. - - name: Pack - run: dotnet pack AppKit.slnx -c Release -p:Platform=x64 -o artifacts/nuget - - - name: Upload packages - uses: actions/upload-artifact@v7 - with: - name: nuget-packages - path: artifacts/nuget/* - - ui-tests: - # FlaUI drives the real WinUI and WPF sample apps on the runner's desktop session. - name: UI end-to-end tests (x64) - runs-on: windows-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up .NET SDK - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - - name: Build solution (x64) - run: dotnet build AppKit.slnx -p:Platform=x64 - - - name: Run UI tests - run: dotnet test tests/KofTwentyTwo.AppKit.UiTests --no-build -p:Platform=x64 - - format: - name: Format (style gate) - runs-on: windows-latest - timeout-minutes: 20 - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up .NET SDK - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore AppKit.slnx -p:Platform=x64 - - # --severity error gates real issues (analyzer errors and error-severity style - # rules), not whitespace preferences. - - name: Verify formatting - run: dotnet format AppKit.slnx --verify-no-changes --severity error --no-restore + ci: + uses: KofTwentyTwo/standards/.github/workflows/dotnet.yml@ae3df7af77e5a97df231a9f53be9d8ed785ddce5 # main (pre-release) + permissions: + contents: read # check out, build, and test; nothing is written + with: + solution: AppKit.slnx + platform: x64 + coverage-packages: KofTwentyTwo.AppKit,KofTwentyTwo.AppKit.Updates + test-projects: | + tests/KofTwentyTwo.AppKit.Tests + ui-test-projects: | + tests/KofTwentyTwo.AppKit.UiTests + pack: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 33dbf74..85be0ae 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,55 +1,29 @@ -# CodeQL static analysis for C#. -# -# C# is a compiled language, so CodeQL needs a real build. We use build-mode -# "manual" and run the same x64 solution build the CI workflow uses (without -# -warnaserror - the CI workflow already gates warnings; this workflow's job is -# security analysis, and it should still scan code that has a pending warning). -name: CodeQL +# CodeQL static analysis (K22-CI-22): security-extended queries for C# (a manual x64 +# build of the solution) and for the workflows themselves, blocking high severity. +name: codeql on: + pull_request: push: branches: [main] - pull_request: - branches: [main] schedule: - # Weekly scan (Mondays 06:30 UTC) so new CodeQL queries run even when the - # code has not changed. - - cron: '30 6 * * 1' + - cron: "41 6 * * 1" -permissions: - contents: read +permissions: {} + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - analyze: - name: Analyze (csharp) - runs-on: windows-latest - timeout-minutes: 120 + codeql: + uses: KofTwentyTwo/standards/.github/workflows/codeql.yml@ae3df7af77e5a97df231a9f53be9d8ed785ddce5 # main (pre-release) permissions: - # Required for github/codeql-action to upload scan results. - security-events: write - # Required to fetch public CodeQL packs. - packages: read - actions: read - contents: read - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up .NET SDK - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: csharp - build-mode: manual - - - name: Build solution (x64) - run: dotnet build AppKit.slnx -p:Platform=x64 - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 - with: - category: '/language:csharp' + contents: read # check out the code + security-events: write # upload CodeQL results to code scanning + with: + languages: '["csharp", "actions"]' + build-mode: manual + build-command: dotnet build AppKit.slnx -p:Platform=x64 + runs-on: windows-latest + setup-dotnet: true diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index e37d2e8..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Blocks pull requests that add dependencies with known vulnerabilities or with -# licenses outside the allowlist below. Only dependencies *added or changed* by -# the PR are checked (via the GitHub dependency graph diff), so existing -# dependencies never retroactively fail this gate. -name: Dependency Review - -on: - pull_request: - -permissions: - contents: read - -jobs: - dependency-review: - name: Dependency review - # No build happens here, so the cheaper Linux runner is fine. - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - # Needed for comment-summary-in-pr. - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Dependency review - uses: actions/dependency-review-action@v5 - with: - # Fail on ANY known vulnerability, even low severity. - fail-on-severity: low - comment-summary-in-pr: on-failure - # Anything not on this list fails the check. AppKit is MIT and is - # consumed by MIT apps, so only permissive licenses are accepted. - # Extend deliberately - a new license here is a legal decision, not a - # build fix. - allow-licenses: >- - MIT, - MIT-0, - Apache-2.0, - BSD-2-Clause, - BSD-3-Clause, - 0BSD, - ISC, - MS-PL, - Unlicense, - CC0-1.0 diff --git a/.github/workflows/pr-gates.yml b/.github/workflows/pr-gates.yml new file mode 100644 index 0000000..fb813c3 --- /dev/null +++ b/.github/workflows/pr-gates.yml @@ -0,0 +1,20 @@ +# Pull request gates (K22-CI-01): Conventional Commit title, DCO sign-off, and +# dependency review, from the shared KofTwentyTwo/standards workflow pinned by SHA +# (K22-CI-11). Dependabot's github-actions ecosystem keeps the pin current. +name: pr-gates + +on: + pull_request: + +permissions: {} + +concurrency: + group: pr-gates-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + pr: + uses: KofTwentyTwo/standards/.github/workflows/pr.yml@ae3df7af77e5a97df231a9f53be9d8ed785ddce5 # main (pre-release) + permissions: + contents: read # read the commits being merged + pull-requests: read # read the pull request title and dependency diff diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2c2c82..9291aa2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,104 +1,81 @@ -# Release pipeline, triggered by pushing a tag that starts with 'v'. +# NuGet package release for a KofTwentyTwo repository (K22-CI-30, K22-REL-*). +# Copy to .github/workflows/release.yml. Pushing a protected v* tag builds, tests, +# packs, generates SBOMs, attests, and publishes the GitHub release through the shared +# reusable workflow, then pushes the packages to nuget.org. # -# v1.2.3 -> stable GitHub release + nuget.org -# v1.2.3-beta.1 -> GitHub prerelease + nuget.org prerelease -# -# Every package is built once, attested (SLSA build provenance), attached to the -# GitHub release with a SHA256SUMS file, then pushed to nuget.org when the -# NUGET_API_KEY secret is configured. See docs/RELEASING.md. -name: Release +# nuget.org trusted publishing (one-time owner setup, no API key stored anywhere): +# nuget.org → your profile → Trusted Publishing → Add policy: +# Repository Owner: KofTwentyTwo +# Repository: AppKit +# Workflow File: release.yml (THIS file's name, not the reusable workflow's) +# Environment: release +# then set the repository variable NUGET_USER to your nuget.org profile name. +# The publish job must stay in this file: nuget.org checks that the OIDC token's +# job_workflow_ref belongs to this repository, which a job inside the reusable +# workflow (in KofTwentyTwo/standards) can never satisfy. +name: release on: push: tags: - - 'v*' + - "v*" -permissions: - contents: read +permissions: {} + +# One release at a time, and never cancel one halfway through publishing. +concurrency: + group: release + cancel-in-progress: false jobs: release: - name: Build, attest, and publish - runs-on: windows-latest - timeout-minutes: 45 + uses: KofTwentyTwo/standards/.github/workflows/release-nuget.yml@ae3df7af77e5a97df231a9f53be9d8ed785ddce5 # main (pre-release) permissions: - contents: write # create the GitHub release and upload assets - id-token: write # sign the provenance attestation (Sigstore OIDC) - attestations: write # store the attestation on the repository - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + contents: write # create and publish the GitHub release + id-token: write # sign attestations + attestations: write # store attestations + with: + solution: AppKit.slnx + platform: x64 + coverage-packages: KofTwentyTwo.AppKit,KofTwentyTwo.AppKit.Updates + test-projects: | + tests/KofTwentyTwo.AppKit.Tests + + publish: + name: publish (nuget.org) + needs: release + if: vars.NUGET_USER != '' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + environment: release + permissions: + contents: read + attestations: read # verify the attestations before pushing + id-token: write # trusted publishing token exchange steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up .NET SDK - uses: actions/setup-dotnet@v5 + - name: Download packages + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - global-json-file: global.json + name: packages + path: packages - - name: Derive version from tag - id: version - shell: pwsh - run: | - $tag = $env:GITHUB_REF_NAME - # Strict semver 2.0; build metadata ('+...') is rejected. - $semver = '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$' - if ($tag -notmatch $semver) { - Write-Error "Tag '$tag' is not a release tag. Expected vMAJOR.MINOR.PATCH with an optional prerelease suffix, e.g. v1.2.3 or v1.2.3-beta.1." - exit 1 - } - $version = $tag.Substring(1) - $prerelease = $version.Contains('-') - Write-Host "Tag: $tag | Version: $version | Prerelease: $prerelease" - Add-Content $env:GITHUB_OUTPUT "version=$version" - Add-Content $env:GITHUB_OUTPUT "prerelease=$($prerelease.ToString().ToLower())" - - # The same gates as CI: a release can never ship what CI would reject. - - name: Build (x64, warnings as errors) - run: dotnet build AppKit.slnx -c Release -p:Platform=x64 -p:Version=${{ steps.version.outputs.version }} -warnaserror - - - name: Unit tests with coverage - run: dotnet test tests/KofTwentyTwo.AppKit.Tests -c Release --no-build -p:Platform=x64 --settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory TestResults - - - name: Enforce 100% line coverage - shell: pwsh - run: ./build/Assert-Coverage.ps1 -ResultsDirectory TestResults -Packages KofTwentyTwo.AppKit, KofTwentyTwo.AppKit.Updates - - - name: Pack - run: dotnet pack AppKit.slnx -c Release --no-build -p:Platform=x64 -p:Version=${{ steps.version.outputs.version }} -o artifacts/nuget - - - name: Checksums - shell: pwsh - run: | - Push-Location artifacts/nuget - Get-ChildItem -File | Sort-Object Name | ForEach-Object { - "{0} {1}" -f (Get-FileHash $_.Name -Algorithm SHA256).Hash.ToLower(), $_.Name - } | Set-Content -Path SHA256SUMS -Encoding ascii - Get-Content SHA256SUMS - Pop-Location - - # Anyone can verify a package came from this workflow at this tag: - # gh attestation verify .nupkg --repo KofTwentyTwo/AppKit - - name: Attest build provenance - uses: actions/attest-build-provenance@v3 - with: - subject-path: 'artifacts/nuget/*.nupkg' - - # Releases are immutable once published, so everything lands on a draft first. - - name: Create draft release with packages - shell: pwsh + # Push only bytes that the shared release workflow built and attested. + - name: Verify attestations env: GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} run: | - $pre = @() - if ('${{ steps.version.outputs.prerelease }}' -eq 'true') { $pre = @('--prerelease') } - gh release create $env:GITHUB_REF_NAME --draft --verify-tag --title "AppKit ${{ steps.version.outputs.version }}" --generate-notes @pre (Get-ChildItem artifacts/nuget -File).FullName + for package in packages/*.nupkg; do + gh attestation verify "$package" --repo "$REPOSITORY" --signer-repo KofTwentyTwo/standards + done - - name: Publish the release - env: - GH_TOKEN: ${{ github.token }} - run: gh release edit $env:GITHUB_REF_NAME --draft=false + - name: NuGet login (OIDC to a one-hour API key) + id: login + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 + with: + user: ${{ vars.NUGET_USER }} - name: Push to nuget.org - if: env.NUGET_API_KEY != '' - run: dotnet nuget push "artifacts/nuget/*.nupkg" --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + env: + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: dotnet nuget push "packages/*.nupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..ed80dc2 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,54 @@ +# OpenSSF Scorecard (K22-CI-24). Scorecard's publishing API only accepts results +# from a workflow shaped exactly like this one: a single job, read-only top-level +# permissions, no top-level env or defaults, and only the allowed steps. +name: scorecard + +on: + branch_protection_rule: + schedule: + - cron: "17 7 * * 1" + push: + branches: [main] + workflow_dispatch: + +permissions: {} + +concurrency: + group: scorecard-${{ github.ref }} + cancel-in-progress: false + +jobs: + analysis: + name: analysis + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + actions: read # Scorecard reads workflow run metadata + security-events: write # upload SARIF to code scanning + id-token: write # publish results to the OpenSSF API and badge + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run Scorecard + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: scorecard.sarif + results_format: sarif + publish_results: true + + - name: Keep the results + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: scorecard-sarif + path: scorecard.sarif + retention-days: 30 + + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 + with: + sarif_file: scorecard.sarif + category: scorecard diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..79d936c --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,25 @@ +# Security scans (K22-CI-20, K22-CI-21, K22-CI-23): gitleaks over full history, +# Trivy + OSV-Scanner over the lock files, zizmor + actionlint over the workflows. +# Runs on every pull request, every push to main, and weekly. +name: security + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: "23 6 * * 1" + workflow_dispatch: + +permissions: {} + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + security: + uses: KofTwentyTwo/standards/.github/workflows/security.yml@ae3df7af77e5a97df231a9f53be9d8ed785ddce5 # main (pre-release) + permissions: + contents: read # check out the code and its history + security-events: write # upload Trivy and zizmor SARIF to code scanning diff --git a/AppKit.slnx b/AppKit.slnx index 0dd3167..1dc4fe6 100644 --- a/AppKit.slnx +++ b/AppKit.slnx @@ -1,28 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CLAUDE.md b/CLAUDE.md index b5e0785..b9e36c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,24 +1,37 @@ # AppKit Shared foundation packages for KofTwentyTwo Windows apps (WinUI 3 and WPF, .NET 10). -Every KofTwentyTwo app depends on this, so treat the public API as a contract. +Every KofTwentyTwo app depends on this, so treat the public API as a contract. The repo +follows the KofTwentyTwo standards (github.com/KofTwentyTwo/standards; local clone +`../standards`). ## Commands ```powershell +dotnet restore AppKit.slnx -p:Platform=x64 --locked-mode dotnet build AppKit.slnx -p:Platform=x64 -warnaserror dotnet test tests/KofTwentyTwo.AppKit.Tests --settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory TestResults ./build/Assert-Coverage.ps1 -ResultsDirectory TestResults -Packages KofTwentyTwo.AppKit,KofTwentyTwo.AppKit.Updates dotnet test tests/KofTwentyTwo.AppKit.UiTests # launches both sample apps on the desktop -dotnet restore AppKit.slnx -p:Platform=x64; dotnet format AppKit.slnx --verify-no-changes --severity error --no-restore +dotnet format AppKit.slnx --verify-no-changes --severity warn --no-restore +pwsh ../standards/tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly ``` ## Rules -- PRs target `dev`; `main` is release-only; tags `v*` publish (see docs/RELEASING.md). +- GitHub Flow: topic branch `/` → PR to `main` → squash merge. No `dev` branch. + Conventional Commit titles, `git commit -s` (DCO), signed commits. Agents never merge, + tag, release, or change repository settings. - Zero warnings; 100% line coverage for `KofTwentyTwo.AppKit` and `KofTwentyTwo.AppKit.Updates`. - Testable logic lives in those two packages; WinUI/WPF packages hold views and adapters only. - UI packages are code-built (no XAML) so they work unpackaged; keep WinUI and WPF in step, with a UI test for both samples. -- Package versions live only in `Directory.Packages.props` (central package management). -- Files are CRLF (`.editorconfig`, `.gitattributes`). +- Kingsrook style (`.editorconfig`): 3-space indent, Allman braces, `if(`, LF line endings, + three blank lines between members, a `///` header comment on every type and method, + flower-box comments inside bodies, SPDX header on every file, one type per file. +- Package versions live only in `Directory.Packages.props`; every project commits its + `packages.lock.json`, and CI restores in locked mode. +- `samples/Shared/*.cs` are linked into both sample projects: run `dotnet format` on the + solution only after checking it did not write merge-conflict markers into them. +- Attack-surface changes update `docs/security/threat-model.md`; expensive-to-reverse + decisions get an ADR in `docs/adr/`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c687f37..ad73a79 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,25 @@ # Contributing to AppKit -AppKit is the shared foundation under every KofTwentyTwo Windows app, so a change here -reaches all of them. That raises the bar: changes are small, tested, and documented. +Thanks for helping. AppKit follows the +[KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards); this page is the +short version of what they ask of a contribution. AppKit sits under every KofTwentyTwo +Windows app, so a change here reaches all of them: changes are small, tested, and +documented. -## Prerequisites +## Before you start -- Windows 11, or Windows 10 version 1809 (build 17763) or later -- [.NET 10 SDK](https://dotnet.microsoft.com/) (`global.json` pins the feature band) -- Visual Studio 2026 with the Windows App SDK workload, optional, for working on the samples +For anything beyond a small fix, open an issue first so the approach can be agreed +before you invest time. Questions and proposals are welcome in +[Issues](https://github.com/KofTwentyTwo/AppKit/issues). -## Branch model +## Setup -- **`dev`** is the integration branch. **Pull requests target `dev`.** -- **`main`** is the release branch. It moves only by reviewed PRs from `dev`, and release - tags are cut from it. - -## Building and testing +Prerequisites: Windows 10 1809 or later, the .NET 10 SDK (`global.json` pins the feature +band), and optionally Visual Studio 2026 or Rider. The commands below are exactly what +CI runs; run them before pushing. ```powershell +dotnet restore AppKit.slnx -p:Platform=x64 --locked-mode dotnet build AppKit.slnx -p:Platform=x64 -warnaserror # Unit tests and the 100% line-coverage gate (KofTwentyTwo.AppKit, KofTwentyTwo.AppKit.Updates) @@ -27,35 +29,88 @@ dotnet test tests/KofTwentyTwo.AppKit.Tests --settings coverage.runsettings --co # End-to-end UI tests: FlaUI launches both sample apps (needs an interactive desktop) dotnet test tests/KofTwentyTwo.AppKit.UiTests -dotnet format AppKit.slnx --verify-no-changes --severity error +# Style gate (Kingsrook layout, analyzers, naming) +dotnet format AppKit.slnx --verify-no-changes --severity warn --no-restore ``` -## What CI enforces +`dotnet format` (without `--verify-no-changes`) fixes layout, `var` use, usings, and +line endings. Two Kingsrook rules it cannot apply are checked in review: three blank +lines between members and a header comment on every type and method (Rider and +ReSharper apply the blank lines from `.editorconfig`). Changing a package version means +updating `Directory.Packages.props` and committing the regenerated `packages.lock.json` +files (`dotnet restore AppKit.slnx -p:Platform=x64 --force-evaluate`). -| Check | Gate | +Install the git hooks once per clone. They run the same secret, workflow, and +commit-message checks CI does: + +```sh +pre-commit install --hook-type pre-commit --hook-type commit-msg +``` + +## Workflow + +1. Branch from `main` with a short-lived topic branch named `/`, + for example `feat/tray-icon` or `fix/42-splash-never-dismisses`. There is no `dev` + branch. +2. Commit with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) + (`feat: ...`, `fix(updates): ...`). Every commit is **signed** and **signed off**: + `git commit -S -s` (see *Sign-off* below). +3. Open a pull request against `main`. Its title must itself be a Conventional Commit + header: pull requests are **squash-merged**, so the title becomes the commit + message on `main`. +4. Every required check must pass, and every review thread must be resolved, before + the pull request merges. + +## What makes a contribution acceptable + +- The build has **zero warnings**, and the format gate is clean. +- New behavior has tests; a bug fix has a test that fails without the fix. +- Line coverage of `KofTwentyTwo.AppKit` and `KofTwentyTwo.AppKit.Updates` stays at + **100%** ([ADR-0003](docs/adr/0003-coverage-scope.md)). Logic goes in those packages; the + WinUI and WPF packages hold views and framework adapters only. +- A shell feature added to the WinUI package gets the same shape in the WPF package and a + UI test in both samples. +- Public API changes are deliberate: a breaking change needs a major version and a note + in the pull request. +- User-visible changes update the docs in the same pull request; attack-surface changes + update the [threat model](docs/security/threat-model.md). +- No secrets, credentials, or personal data, ever. +- New dependencies are justified in the pull request description. + +## Checks that gate a merge + +| Check | What it verifies | | --- | --- | -| **Build and test (x64)** | Zero-warning build (NuGetAudit included), unit tests, **100% line coverage** of the two framework-free packages, and every package still packs | -| **UI end-to-end tests (x64)** | Splash, About, crash net, log window, settings, and the update flow, in both the WinUI and WPF samples | -| **Format (style gate)** | `dotnet format --verify-no-changes --severity error` | -| **Dependency review** | New dependencies: no known vulnerabilities, permissive licenses only | -| **CodeQL** | Static security analysis on PRs to `main` and weekly | +| `pr / title` | The PR title is a Conventional Commit header | +| `pr / dco` | Every commit is signed off | +| `pr / dependency-review` | Added dependencies have no known vulnerabilities and an allowed license | +| `security / secrets`, `security / sca`, `security / workflows` | No secrets, no vulnerable or malicious dependencies, safe workflows | +| `codeql / analyze (csharp)`, `codeql / analyze (actions)` | No high-severity static analysis findings | +| `ci / build-test` | Locked restore, zero-warning build, unit tests, 100% coverage gate, packages pack | +| `ci / format` | `dotnet format --verify-no-changes --severity warn` | +| `ci / ui-tests` | FlaUI end-to-end tests of both sample apps | -## Design rules +## Sign-off (Developer Certificate of Origin) -- **Logic goes in the framework-free packages.** If code can be tested without a UI, it - lives in `KofTwentyTwo.AppKit` or `KofTwentyTwo.AppKit.Updates` and is covered there. - The WinUI and WPF packages hold views and framework adapters only. -- **UI packages are built in code, not XAML.** XAML compiled into a library has to be - merged into the host app's resources, which is fragile for unpackaged apps. -- **Nothing optional may crash the app.** Logging, settings, and updates report failures - as values or log entries, never as exceptions escaping to the user. -- **WinUI and WPF stay in step.** A shell feature added to one package gets the same - shape in the other, and a UI test in both samples. -- **Never write a secret to disk, logs, or process output.** Secrets go through - `ISecretVault`. -- **Breaking changes are deliberate.** Every app depends on this API. A breaking change - needs a major version bump and a note in the release. +By signing off a commit you certify the +[Developer Certificate of Origin 1.1](https://developercertificate.org/): that you +wrote the change, or otherwise have the right to submit it under the project's +license. `git commit -s` appends the sign-off: + +```text +Signed-off-by: Your Name +``` + +The name and email must match the commit author. To add missing sign-offs to a +branch, run `git rebase --signoff main` and force-push the branch. + +## AI-assisted contributions + +AI coding tools are welcome. You remain the author: you must understand, test, and be +able to explain every line you submit, and you sign it off as your own. Note +substantial AI assistance in the pull request description. ## License -By contributing, you agree that your contributions are licensed under the [MIT License](LICENSE). +By contributing, you agree that your contributions are licensed under the project's +[MIT License](LICENSE). diff --git a/Directory.Build.props b/Directory.Build.props index 850b033..25b5fe8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,45 +1,72 @@ - - - true - latest-recommended - true - true - enable - enable - latest - false - + + + enable + enable + latest + false - - - 0.1.0 - dev - + + true + true + latest-recommended + true + + true - - - James Maes - Copyright (c) 2026 James Maes - MIT - https://github.com/KofTwentyTwo/AppKit - https://github.com/KofTwentyTwo/AppKit - git - windows;desktop;winui;wpf;velopack - true - snupkg - true - + + true + true + true + true + snupkg - - - true - + + true + all + low + true + true + + + + + true + true + + + + + $(NoWarn);CS1591 + + + + + + + + + + 0.1.0 + dev + + + + + James Maes + Copyright (c) 2026 James Maes (KofTwentyTwo) + MIT + https://github.com/KofTwentyTwo/AppKit + https://github.com/KofTwentyTwo/AppKit + git + windows;desktop;winui;wpf;velopack + diff --git a/Directory.Packages.props b/Directory.Packages.props index ad80d0b..b870b05 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,24 +1,30 @@ - - - true - + + + true + true + - - - - - - + + + - - - - - - - + + + + + + + + + + + + + + diff --git a/README.md b/README.md index abf4f37..6df03ae 100644 --- a/README.md +++ b/README.md @@ -7,44 +7,71 @@ log viewer, a crash safety net, theming, and Velopack self-update with stable/de channels. Extracted from [gclo](https://github.com/KofTwentyTwo/gclo), where every piece shipped in a 1.0 release first. +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/KofTwentyTwo/AppKit/badge)](https://scorecard.dev/viewer/?uri=github.com/KofTwentyTwo/AppKit) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) ![.NET 10](https://img.shields.io/badge/.NET-10-blueviolet) ![Platform: Windows](https://img.shields.io/badge/platform-Windows%2010%2F11-blue) +| | | +| --- | --- | +| **Tier** | Product ([scope](https://github.com/KofTwentyTwo/standards/blob/main/policies/README.md#scope)) | +| **Standards** | Adopting [KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards) (pre-v1.0); this README will say *Conforms to KofTwentyTwo standards v1.0* once the conformance checker passes | +| **Latest release** | [Releases](https://github.com/KofTwentyTwo/AppKit/releases/latest) (none yet) | + ## Packages | Package | Target | What it gives you | | --- | --- | --- | | `KofTwentyTwo.AppKit` | `net10.0` | `AppInfo` identity, `AppPaths`, `BuildVersion`, `FileActivityLog` + `LogTail`, `SettingsStore` + `ShellSettings`, `CredentialManagerVault`, `IUserPrompter` | | `KofTwentyTwo.AppKit.Updates` | `net10.0` | `VelopackStartup`, `VelopackUpdateService` (GitHub Releases, stable/dev channel), `UpdateCoordinator` (the "Check for updates" flow) | -| `KofTwentyTwo.AppKit.WinUI` | `net10.0-windows10.0.19041.0` | `SplashOverlay`, `AboutDialog`, `LogWindow`, `DialogGuard`, `ContentDialogPrompter`, `WinUIShell` (crash net, theme), DPI-aware window helpers | +| `KofTwentyTwo.AppKit.WinUI` | `net10.0-windows10.0.19041.0` | `SplashOverlay`, `AboutDialog`, `LogWindow`, `DialogGuard`, `ContentDialogPrompter`, `WinUIShell` (crash net, theme), DPI-aware `WindowExtensions` | | `KofTwentyTwo.AppKit.Wpf` | `net10.0-windows` | `SplashOverlay`, `AboutWindow`, `LogWindow`, `MessageBoxPrompter`, `WpfShell` (crash net, Fluent theme) | -The two framework-free packages are held to **100% line coverage** in CI. The UI packages -are built in code rather than XAML, so they work identically in packaged (MSIX) and -unpackaged (Velopack) apps, and are exercised end-to-end by FlaUI tests that drive the -sample apps. +The two UI-framework-free packages are held to **100% line coverage** in CI. The UI +packages are built in code rather than XAML, so they work identically in packaged (MSIX) +and unpackaged (Velopack) apps, and are exercised end-to-end by FlaUI tests that drive +the sample apps ([ADR-0001](docs/adr/0001-code-built-ui.md)). -## Using it +## Install -New apps should start from the app template rather than wiring this by hand; it -references these packages and has everything below already in place. To add AppKit to -an existing WinUI app: +The packages are published to nuget.org with the GitHub release of each version. +Reference them through central package management: + +```xml + + + + + + +``` + +The UI packages bring in `KofTwentyTwo.AppKit` and `KofTwentyTwo.AppKit.Updates`. A +WinUI app references `Microsoft.WindowsAppSDK` itself (the WinUI package does not carry +its build targets) and copies its brand assets (`Assets/Brand/app-icon.svg`, +`app.ico`) to the output directory. + +## Usage + +New apps should start from the KofTwentyTwo Windows app template, which has everything +below in place; the design is the +[Windows desktop reference architecture](https://github.com/KofTwentyTwo/standards/blob/main/architecture/windows-desktop-app.md). +To add AppKit to an existing WinUI app: ```csharp // One identity, declared once. -static class MyApp +internal static class MyApp { - public static AppInfo Info { get; } = new() - { - Id = "myapp", // data folder, log prefix, credential prefix - DisplayName = "My App", - Tagline = "Does one thing well", - RepositoryUrl = new Uri("https://github.com/KofTwentyTwo/myapp"), // also the update feed - Author = "James Maes", - Copyright = "Copyright (c) 2026 James Maes", - }; - public static AppPaths Paths { get; } = new(Info); + public static AppInfo Info { get; } = new() + { + Id = "myapp", // data folder, log prefix, credential prefix + DisplayName = "My App", + Tagline = "Does one thing well", + RepositoryUrl = new Uri("https://github.com/KofTwentyTwo/myapp"), // also the update feed + Author = "James Maes", + Copyright = "Copyright (c) 2026 James Maes", + }; + public static AppPaths Paths { get; } = new(Info); } // Program.Main (with DISABLE_XAML_GENERATED_MAIN): Velopack goes first. @@ -57,41 +84,74 @@ WinUIShell.ApplyTheme(this, settings.ThemeKind); // Main window SplashOverlay.ShowOver(RootGrid, MyApp.Info, settings); var updates = new UpdateCoordinator( - new VelopackUpdateService(MyApp.Info.RepositoryUrl!), - new ContentDialogPrompter(() => Content.XamlRoot), - MyApp.Info.DisplayName, Log); + new VelopackUpdateService(MyApp.Info.RepositoryUrl!), + new ContentDialogPrompter(() => Content.XamlRoot), + MyApp.Info.DisplayName, Log); + // Help menu await updates.CheckInteractivelyAsync(); await AboutDialog.ShowAsync(MyApp.Info, Content.XamlRoot); LogWindow.ShowSingle(Log, MyApp.Info); ``` -Apps reference `Microsoft.WindowsAppSDK` themselves (the WinUI package does not carry its -build targets), and copy their brand assets (`Assets/Brand/app-icon.svg`, `app.ico`) to -the output directory. See [`samples/`](samples) for complete WinUI and WPF apps. +[`samples/`](samples) has complete WinUI and WPF apps wired to every service. -## Repository layout +## Building from source -``` -src/ the four packages -tests/ unit tests (100% gate) and FlaUI end-to-end tests of the samples -samples/ minimal WinUI and WPF apps wired to every service -tools/ BrandTool: generates app-icon.svg and a multi-size app.ico from one definition -build/ scripts CI shares with every app repo (coverage gate) -``` +Prerequisites: -## Building +- Windows 10 1809 or later (the UI packages and samples are Windows-only) +- .NET 10 SDK (the feature band is pinned by `global.json`) +- Optional: Visual Studio 2026 or Rider for the samples ```powershell +dotnet restore AppKit.slnx -p:Platform=x64 --locked-mode dotnet build AppKit.slnx -p:Platform=x64 -warnaserror # zero warnings, always dotnet test tests/KofTwentyTwo.AppKit.Tests --settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory TestResults ./build/Assert-Coverage.ps1 -ResultsDirectory TestResults -Packages KofTwentyTwo.AppKit,KofTwentyTwo.AppKit.Updates dotnet test tests/KofTwentyTwo.AppKit.UiTests # drives the samples; needs a desktop -dotnet format AppKit.slnx --verify-no-changes --severity error +dotnet format AppKit.slnx --verify-no-changes --severity warn --no-restore +dotnet pack AppKit.slnx -c Release -p:Platform=x64 -o artifacts/nuget ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for the branch model and PR expectations, and -[docs/RELEASING.md](docs/RELEASING.md) for how packages are published. +Dependencies are declared in `Directory.Packages.props` (central package management) +and locked per project in `packages.lock.json`; see [CONTRIBUTING.md](CONTRIBUTING.md) +for the full local workflow. + +```text +src/ the four packages +tests/ unit tests (100% gate) and FlaUI end-to-end tests of the samples +samples/ minimal WinUI and WPF apps wired to every service +tools/ BrandTool: generates app-icon.svg and a multi-size app.ico from one definition +build/ Assert-Coverage.ps1, the coverage gate +docs/ release process, architecture decisions, threat model +``` + +## Verifying a release + +Every release asset (`.nupkg`, `.snupkg`, and the CycloneDX SBOMs) is listed in +`SHA256SUMS` and has a build-provenance attestation signed by the shared KofTwentyTwo +release workflow: + +```powershell +gh release download v0.1.0 --repo KofTwentyTwo/AppKit +sha256sum --check SHA256SUMS +gh attestation verify KofTwentyTwo.AppKit.0.1.0.nupkg --repo KofTwentyTwo/AppKit --signer-repo KofTwentyTwo/standards +``` + +## Support + +Only the latest released minor version receives fixes, including security fixes; see +[SECURITY.md](SECURITY.md#supported-versions). + +## Security + +Report vulnerabilities privately; see [SECURITY.md](SECURITY.md). The attack surface is +described in the [threat model](docs/security/threat-model.md). + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). Releases: [docs/RELEASING.md](docs/RELEASING.md). ## License diff --git a/SECURITY.md b/SECURITY.md index 7f6a737..6f3fa70 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,33 +1,53 @@ # Security Policy -## Supported versions - -Only the **latest release** of the AppKit packages receives security fixes. Apps built on -AppKit pick up a fix by updating their package reference (Dependabot opens that PR). - ## Reporting a vulnerability -**Please do not open a public issue for security problems.** +**Do not open a public issue for a security problem.** -This repository has GitHub **private vulnerability reporting** enabled: +Report it privately through GitHub's +[private vulnerability reporting](https://github.com/KofTwentyTwo/AppKit/security/advisories/new) +(the repository's **Security** tab, then **Report a vulnerability**). That is the only +reporting channel. -1. Go to the repository's [Security tab](https://github.com/KofTwentyTwo/AppKit/security). -2. Click **Report a vulnerability**. -3. Describe the issue, how to reproduce it, and the impact you see. +Security contact: **James Maes** ([@KofTwentyTwo](https://github.com/KofTwentyTwo)), +maintainer. -That is the only reporting channel. AppKit is maintained by one person, so responses are -best-effort: normally an acknowledgment within **7 days**. +Please include the affected package and version, how to reproduce it, and the impact +you see. AppKit's attack surface is described in its +[threat model](docs/security/threat-model.md). -## Security-relevant design +## What to expect -- **Secrets** (tokens, keys) are stored only through `ISecretVault`. The Windows - implementation uses the per-user Credential Manager (DPAPI-protected) and clears its - plaintext buffer after writing. Secrets never reach settings files or logs. -- **Updates** come only from the app's own GitHub Releases over HTTPS via Velopack, and - each package's integrity is checked by Velopack before it is applied. -- **Releases** of AppKit carry SLSA build-provenance attestations and a `SHA256SUMS` file. - Verify a package with: +KofTwentyTwo follows coordinated vulnerability disclosure. Timeframes are counted +from the day the report arrives and come from the +[KofTwentyTwo security program](https://github.com/KofTwentyTwo/standards/blob/main/policies/security-program.md) +(targets until the standards reach v1.0.0, commitments from then on): - ```powershell - gh attestation verify KofTwentyTwo.AppKit..nupkg --repo KofTwentyTwo/AppKit - ``` +| Step | Target | +| --- | --- | +| Acknowledge the report | 7 days | +| Assess it: confirm or reject, with a severity | 14 days | +| Release a fix for a CRITICAL vulnerability | 7 days | +| Release a fix for a HIGH vulnerability | 30 days | +| Release a fix for a MEDIUM vulnerability | 90 days | +| Release a fix for a LOW vulnerability | The next release | +| Publish the advisory (GitHub Security Advisory, CVE where applicable) | When the fix ships, or 90 days after the report, whichever comes first | + +Reporters are credited in the advisory unless they ask not to be. + +## Supported versions + +| Version | Security fixes | +| --- | --- | +| Latest minor release (`0.1.x` once published) | Yes | +| Older | No; upgrade to the latest release | + +All four packages release together under one version. Only the latest minor release +receives fixes; a line stops receiving security fixes when the next minor version is +released. After a new major version, the previous major's last minor receives security +fixes for 6 months. + +## Published vulnerabilities + +Fixed vulnerabilities are published as +[GitHub Security Advisories](https://github.com/KofTwentyTwo/AppKit/security/advisories). diff --git a/coverage.runsettings b/coverage.runsettings index 15cd098..de28395 100644 --- a/coverage.runsettings +++ b/coverage.runsettings @@ -1,21 +1,21 @@ - - - - - cobertura - [KofTwentyTwo.AppKit]*,[KofTwentyTwo.AppKit.Updates]* - GeneratedCodeAttribute,ExcludeFromCodeCoverageAttribute - - - - + + + + + cobertura + [KofTwentyTwo.AppKit]*,[KofTwentyTwo.AppKit.Updates]* + GeneratedCodeAttribute,ExcludeFromCodeCoverageAttribute + + + + diff --git a/docs/RELEASING.md b/docs/RELEASING.md index e8d0b95..ce6e5e0 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,8 +1,14 @@ # Releasing AppKit -Releases are automated by [`.github/workflows/release.yml`](../.github/workflows/release.yml). -Pushing a `v*` tag builds, tests, packs, attests, and publishes all four packages -together; they always share one version. +Releases follow the +[KofTwentyTwo release standard](https://github.com/KofTwentyTwo/standards/blob/main/standards/releases.md). +Pushing a protected `v*` tag on `main` runs [`.github/workflows/release.yml`](../.github/workflows/release.yml), +which calls the shared `release-nuget.yml` workflow in KofTwentyTwo/standards. All four +packages always release together under one version. + +> **Status:** the shared workflows are pinned to a commit on KofTwentyTwo/standards +> `main` while the standards are pre-release; they move to a release tag's commit once +> KofTwentyTwo/standards publishes `v1.0.0` (Dependabot proposes the update). ## Versioning @@ -15,56 +21,69 @@ The version is the tag without the leading `v`, in strict [semver](https://semve AppKit is a library every app depends on, so semver is a promise: -- **major**: any breaking change to a public API (renames, removed members, changed behavior apps rely on) +- **major**: any breaking change to a public API - **minor**: new capabilities, backward compatible - **patch**: fixes only -`Directory.Build.props` holds `VersionPrefix`, the next expected release. Local builds are -`-dev`. Bump the prefix on `dev` right after each stable release. +`Directory.Build.props` holds `VersionPrefix`, the next expected release; local builds +are `-dev`. Bump the prefix right after each release, in a pull request. -## What a release publishes +## What a release does -| Target | Condition | -| --- | --- | -| GitHub Release: the four `.nupkg` files, their `.snupkg` symbol packages, `SHA256SUMS` | always | -| Build-provenance attestation for every `.nupkg` | always | -| nuget.org | the `NUGET_API_KEY` repository secret is set | +The shared workflow (SLSA Build L3: the build definition lives in KofTwentyTwo/standards, +not here): -`NUGET_API_KEY` is a nuget.org API key with **push** scope limited to the glob -`KofTwentyTwo.AppKit*`. Set it under **Settings → Secrets and variables → Actions**. -Without it, the release still succeeds and the packages are only on GitHub. +1. Validates the tag and re-runs every gate that guards `main`: locked restore, + zero-warning build, unit tests, and the 100% coverage gate. +2. Packs the four packages and their symbol packages. +3. Generates a CycloneDX 1.6 SBOM per package (Syft, from `packages.lock.json`) and + attests it. +4. Writes `SHA256SUMS` over every asset and attests build provenance. +5. Creates a draft GitHub Release with the assets and generated notes, then publishes it + (releases are immutable). + +Then the `publish` job in this repository's `release.yml` downloads the packages, +verifies their attestations were signed by the shared workflow, and pushes them to +nuget.org through **trusted publishing** (OIDC; no API key is stored anywhere). + +## One-time setup (owner) + +1. **nuget.org trusted publishing policy:** nuget.org → your profile → *Trusted + Publishing* → *Add policy*: + - Repository owner: `KofTwentyTwo` + - Repository: `AppKit` + - Workflow file: `release.yml` + - Environment: `release` +2. **Repository variable** `NUGET_USER` (Settings → Secrets and variables → Actions → + Variables): your nuget.org profile name. Without it the `publish` job is skipped and + the release still succeeds on GitHub. +3. **Environment** `release` (Settings → Environments) with a deployment rule that allows + only tags matching `v*`. ## Cutting a release -1. Merge `dev` into `main` through a PR, with every check green. -2. Tag `main` and push the tag: +1. Make sure `main` is green and the conformance check passes: + `pwsh ../standards/tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/AppKit -LocalPath .` +2. For a minor or major release, open a release checklist issue from the standards + template and complete it (security assessment, threat model review, scan results). +3. Tag `main` and push the tag: ```powershell - git checkout main + git switch main git pull - git tag v0.1.0 + git tag -s v0.1.0 -m "v0.1.0" git push origin v0.1.0 ``` -3. Watch the **Release** workflow. It re-runs the zero-warning build, the unit tests, and - the coverage gate, then packs, writes checksums, attests, creates a **draft** release - with the packages and generated notes, publishes it, and pushes to nuget.org. -4. Verify: the release lists eight package files plus `SHA256SUMS`, and - `gh attestation verify .nupkg --repo KofTwentyTwo/AppKit` succeeds. -5. On `dev`, bump `VersionPrefix` in `Directory.Build.props`. +4. Watch the **release** workflow, then verify the release as described in the + [README](../README.md#verifying-a-release). +5. Open a pull request that bumps `VersionPrefix` in `Directory.Build.props`. ## When something fails -- **Before "Publish the release"**: nothing is public. Delete the draft +- **Before the release is published:** nothing is public. Delete the draft (`gh release delete v0.1.0 --yes`), fix the cause, and re-run the workflow. -- **At "Push to nuget.org"**: the GitHub release is published and immutable, so do not - re-run the job. Push by hand from the release's assets: - - ```powershell - gh release download v0.1.0 --pattern "*.nupkg" --dir nupkgs - dotnet nuget push "nupkgs/*.nupkg" --api-key --source https://api.nuget.org/v3/index.json --skip-duplicate - ``` - -- **A published release is broken**: never reuse its version. nuget.org versions are - permanent (they can only be unlisted). Fix forward with a new patch release, and unlist - the bad version on nuget.org. +- **At `publish (nuget.org)`:** the GitHub release is published and immutable; re-run + only the failed `publish` job from the Actions page. +- **A published release is broken:** never reuse its version. Fix forward with a new + patch release, and unlist the bad version on nuget.org. diff --git a/docs/adr/0001-code-built-ui.md b/docs/adr/0001-code-built-ui.md new file mode 100644 index 0000000..98fa937 --- /dev/null +++ b/docs/adr/0001-code-built-ui.md @@ -0,0 +1,40 @@ +# ADR-0001: Build the WinUI and WPF views in code, not XAML + +- **Status:** Accepted +- **Date:** 2026-10-09 +- **Pull request:** recorded retroactively for the initial design + +## Context + +The splash, About, and log viewer screens are shared by every KofTwentyTwo app through +NuGet packages. Most of those apps ship unpackaged through Velopack, not as MSIX. A +WinUI XAML library compiles its markup into `.xbf` and `.pri` resources that the host +app must merge into its own resource index; that merge is reliable for packaged apps +and fragile for unpackaged ones (missing-resource crashes at runtime that no build +step catches). + +## Options considered + +### Option A: XAML user controls in the libraries + +- Pros: familiar authoring, designer support. +- Cons: resource merging in unpackaged hosts; two build pipelines (WinUI and WPF XAML + compilers) inside libraries; harder to keep the WinUI and WPF versions identical. + +### Option B: views built in C# + +- Pros: a plain assembly with no resources to merge, so it works the same packaged or + not; WinUI and WPF versions can mirror each other line for line. +- Cons: layout is more verbose to read and write. + +## Decision + +Option B. The UI packages construct their visual trees in C#. Host apps still use XAML +for their own windows. + +## Consequences + +- The packages work in packaged and unpackaged apps without extra setup. +- Views are a little harder to restyle; theme resources are looked up at runtime. +- Security: none. +- The FlaUI tests drive the sample apps to prove the views render and behave. diff --git a/docs/adr/0002-velopack-in-separate-package.md b/docs/adr/0002-velopack-in-separate-package.md new file mode 100644 index 0000000..a5b7a79 --- /dev/null +++ b/docs/adr/0002-velopack-in-separate-package.md @@ -0,0 +1,39 @@ +# ADR-0002: Isolate Velopack in KofTwentyTwo.AppKit.Updates + +- **Status:** Accepted +- **Date:** 2026-10-09 +- **Pull request:** recorded retroactively for the initial design + +## Context + +Self-update through Velopack is right for per-user desktop apps, but not every +KofTwentyTwo app should carry it: a Windows service is installed by an MSI and updated +through winget or the MSI, and a library consumer may want identity and logging +without any update machinery. + +## Options considered + +### Option A: Velopack inside KofTwentyTwo.AppKit + +- Pros: one package fewer. +- Cons: every consumer, including services and CLIs, pulls in Velopack and its native + update hooks. + +### Option B: a separate KofTwentyTwo.AppKit.Updates package + +- Pros: the core package stays dependency-free; update logic (`UpdateCoordinator`) is + still shared by WinUI and WPF; apps that do not self-update simply do not reference it. +- Cons: one more package to version (all four release together, so this costs little). + +## Decision + +Option B. Velopack and the update flow live in `KofTwentyTwo.AppKit.Updates`; the UI +packages reference it because their apps self-update. + +## Consequences + +- `KofTwentyTwo.AppKit` has no third-party runtime dependency. +- The Velopack pass-through is isolated behind `IUpdateBackend`, so every decision + around it is unit-tested and only the thin adapter is excluded from coverage. +- Security: the update feed is an attack surface only for apps that reference this + package (see the threat model, T1–T3). diff --git a/docs/adr/0003-coverage-scope.md b/docs/adr/0003-coverage-scope.md new file mode 100644 index 0000000..989d196 --- /dev/null +++ b/docs/adr/0003-coverage-scope.md @@ -0,0 +1,41 @@ +# ADR-0003: Hold the UI-framework-free packages at 100% line coverage + +- **Status:** Accepted +- **Date:** 2026-10-09 +- **Pull request:** recorded retroactively for the initial design + +## Context + +The KofTwentyTwo testing standard requires 100% line coverage for libraries published +for others to consume and for UI-free core logic +([K22-TEST-10](https://github.com/KofTwentyTwo/standards/blob/main/standards/testing.md#coverage)). +WinUI and WPF views need a live compositor and a desktop session, which unit tests do +not have. + +## Options considered + +### Option A: measure every package + +- Pros: one number. +- Cons: the view packages cannot reach 100% without a desktop, so the gate would be + lowered for everyone or riddled with exclusions. + +### Option B: measure KofTwentyTwo.AppKit and KofTwentyTwo.AppKit.Updates at 100% + +- Pros: the gate is meaningful and cheap to keep; it pushes logic out of the views and + into tested code. +- Cons: the view packages rely on the FlaUI end-to-end tests instead of a number. + +## Decision + +Option B. `coverage.runsettings` measures only the two framework-free packages, and +`build/Assert-Coverage.ps1` fails CI below 100%. Exclusions use +`[ExcludeFromCodeCoverage(Justification = ...)]` only for thin pass-throughs that need +an installed app or the network. + +## Consequences + +- New logic belongs in the framework-free packages; views stay thin. +- The UI packages are verified by the sample apps' UI tests in CI. +- Security: none directly; tested code paths include the secret vault and the update + guarding. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md new file mode 100644 index 0000000..4f9450c --- /dev/null +++ b/docs/security/threat-model.md @@ -0,0 +1,93 @@ +# Threat model: AppKit + +AppKit is a set of libraries, so its threats are the ones it creates inside the apps +that use it. This model covers every external interface the four packages open; each +app's own threat model starts from this one ([K22-SDLC-03](https://github.com/KofTwentyTwo/standards/blob/main/policies/sdlc.md#plan-and-design)). + +- **Version reviewed:** 0.1.0-dev (before the first release) · **Last reviewed:** 2026-10-09 + +## 1. What the product is and does + +Four NuGet packages that give a KofTwentyTwo Windows desktop app its identity, per-user +data paths, logging, settings, secret storage, splash/About/log-viewer UI, crash net, +theming, and Velopack self-update from the app's GitHub Releases. AppKit runs inside +the host app's process, with the logged-in user's rights; it has no server side and +opens no listening sockets. + +## 2. Actors and components + +| Actor or component | Description | Trust level | +| --- | --- | --- | +| User | The person running the host app | Trusted for their own data | +| Host app | The application that references AppKit | Trusted (same process) | +| Other local processes of the same user | Can read and write the user's files | Not trusted, but AppKit cannot defend against them | +| GitHub Releases (update feed) | Serves release metadata and Velopack packages over HTTPS | Untrusted network content; integrity checked | +| Velopack | Downloads, verifies, and applies update packages | Trusted dependency | +| Windows Credential Manager | Stores secrets per user, protected by DPAPI | Trusted OS component | + +## 3. Data flow and trust boundaries + +```mermaid +flowchart LR + user([User]) --> app[Host app + AppKit] + app -- HTTPS --> gh[(GitHub Releases)] + app --> settings[(settings.json)] + app --> logs[(logs/*.log)] + app --> vault[(Credential Manager)] + app -. opens .-> browser[Default browser] + subgraph machine [Trust boundary: the user's Windows session] + app + settings + logs + vault + end +``` + +## 4. External interfaces + +| Interface | Direction | Data | Authenticated? | Validated where? | +| --- | --- | --- | --- | --- | +| Update check and download (`VelopackUpdateService`) | In | Release metadata, update packages | HTTPS to github.com; Velopack verifies package hashes from the release feed | Velopack; versions parsed by Velopack | +| `settings.json` (`SettingsStore`) | In and out | User preferences | No (user-writable file) | `ISanitizable.Sanitize` clamps and validates every value on load; a corrupt file yields defaults | +| Log files (`FileActivityLog`) | Out (and in for `LogTail`) | Operational messages | No (user-writable files) | Viewers display text only; nothing is executed or parsed beyond line timestamps (regex with a timeout) | +| Credential Manager (`CredentialManagerVault`) | In and out | Secrets the host app stores (tokens) | Windows logon session (DPAPI) | Keys validated; secrets never logged | +| `_DATA_DIR` environment variable (`AppPaths`) | In | Data folder path | No | Used as a folder path only | +| Velopack process hooks (`VelopackStartup`) | In | Command-line arguments from the installer/updater | No | Velopack handles them and exits; other arguments pass through untouched | +| Links in the About dialog and log window | Out | Repository URL, log folder path | No | Values come from the host app's `AppInfo`, not from input | + +## 5. Assets + +- The user's secrets in the Credential Manager (for example GitHub tokens in gclo). +- The integrity of the host app's code: a malicious update would run as the user. +- The user's data and preferences under `%LOCALAPPDATA%\`. +- The availability of the host app (a crash or hang at startup). + +## 6. Threats (STRIDE) + +| # | Boundary / component | Category | Threat | Likelihood | Impact | Mitigation | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| T1 | Update feed | Tampering | An attacker serves a malicious update package | Low | Critical | HTTPS only; Velopack verifies package hashes; releases are immutable and every asset is attested by the shared release workflow (SLSA Build L3); prerelease builds follow only the dev channel | Mitigated | +| T2 | Update feed | Spoofing | A look-alike repository URL is configured | Low | Critical | The feed is the host app's own `AppInfo.RepositoryUrl`, fixed at build time, not user input | Mitigated | +| T3 | Update flow | Denial of service | A failing or slow update check crashes or blocks the app | Medium | Medium | Every update member is guarded and returns errors as values; the check runs after startup | Mitigated | +| T4 | Secrets | Information disclosure | A token leaks through logs, settings, or memory | Medium | High | Secrets only in the Credential Manager; never in settings or logs; the pinned plaintext buffer is cleared after writing | Mitigated | +| T5 | settings.json | Tampering | A crafted settings file sets out-of-range values or crashes parsing | Low | Low | Source-generated JSON parsing inside a catch-all; values sanitized; failure means defaults | Mitigated | +| T6 | Log files | Tampering / denial of service | A huge or crafted log file hangs the log viewer | Low | Low | The viewer reads only the last 2,000 lines; the entry regex has a match timeout | Mitigated | +| T7 | UI thread | Denial of service | An exception escaping an async handler kills the app | Medium | Medium | Crash net logs and handles dispatcher exceptions; one-dialog-at-a-time guard prevents the stowed-exception crash | Mitigated | +| T8 | Data folder | Elevation of privilege | `_DATA_DIR` points the app at another location | Low | Low | The variable is set by the same user; the app gains no rights it did not have | Accepted | +| T9 | Dependencies | Tampering | A compromised package in AppKit's graph | Low | High | Central versions, lock files with locked restore, dependency review, Trivy and OSV-Scanner in CI, Dependabot with a cooldown | Mitigated | +| T10 | Repudiation | Repudiation | Not applicable: AppKit makes no security decisions on behalf of a remote party | n/a | n/a | n/a | n/a | + +## 7. Accepted risks + +- **T8:** another process running as the same user can redirect or read AppKit's data; + defending against the user's own processes is outside a desktop library's reach. +- **Unsigned binaries until code signing is available:** apps built on AppKit ship + without an Authenticode signature until the KofTwentyTwo signing identity is validated + ([exception EX-0002](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0002)); + provenance attestations and checksums remain verifiable. + +## 8. Review log + +| Date | Version | Reviewer | Changes | +| --- | --- | --- | --- | +| 2026-10-09 | 0.1.0-dev | James Maes | Initial model | diff --git a/samples/AppKit.Sample.WinUI/App.xaml b/samples/AppKit.Sample.WinUI/App.xaml index 60cc6eb..8ca222b 100644 --- a/samples/AppKit.Sample.WinUI/App.xaml +++ b/samples/AppKit.Sample.WinUI/App.xaml @@ -1,13 +1,13 @@ - - - - - - - + x:Class="AppKit.Sample.WinUI.App" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + + diff --git a/samples/AppKit.Sample.WinUI/App.xaml.cs b/samples/AppKit.Sample.WinUI/App.xaml.cs index f7280d9..5525c29 100644 --- a/samples/AppKit.Sample.WinUI/App.xaml.cs +++ b/samples/AppKit.Sample.WinUI/App.xaml.cs @@ -1,28 +1,41 @@ +/* + * Copyright (c) 2026 James Maes (KofTwentyTwo) + * SPDX-License-Identifier: MIT + */ + using KofTwentyTwo.AppKit.Logging; using KofTwentyTwo.AppKit.WinUI; using Microsoft.UI.Xaml; + namespace AppKit.Sample.WinUI; +/// The WinUI sample application: installs AppKit's crash net and the saved theme, then opens the main window. public partial class App : Application { - private Window? _window; + private Window? _window; - /// The app-wide activity log. - internal static IActivityLog Log { get; } = FileActivityLog.ForApp(SampleApp.Info, SampleApp.Paths); + /// The app-wide activity log. + internal static IActivityLog Log { get; } = FileActivityLog.ForApp(SampleApp.Info, SampleApp.Paths); - public App() - { - InitializeComponent(); - WinUIShell.InstallCrashNet(this, () => Log); - // Only settable here, before any window exists. - WinUIShell.ApplyTheme(this, SampleApp.SettingsStore.Load().ThemeKind); - } - protected override void OnLaunched(LaunchActivatedEventArgs args) - { - Log.Info($"{SampleApp.Info.DisplayName} started."); - _window = new MainWindow(); - _window.Activate(); - } + + /// Installs the crash net and applies the saved theme; WinUI allows setting the application theme only here, before any window exists. + public App() + { + InitializeComponent(); + WinUIShell.InstallCrashNet(this, () => Log); + // Only settable here, before any window exists. + WinUIShell.ApplyTheme(this, SampleApp.SettingsStore.Load().ThemeKind); + } + + + + /// Logs the start and opens the main window. + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + Log.Info($"{SampleApp.Info.DisplayName} started."); + _window = new MainWindow(); + _window.Activate(); + } } diff --git a/samples/AppKit.Sample.WinUI/AppKit.Sample.WinUI.csproj b/samples/AppKit.Sample.WinUI/AppKit.Sample.WinUI.csproj index c8025e0..19b27ab 100644 --- a/samples/AppKit.Sample.WinUI/AppKit.Sample.WinUI.csproj +++ b/samples/AppKit.Sample.WinUI/AppKit.Sample.WinUI.csproj @@ -1,38 +1,38 @@ - - - WinExe - net10.0-windows10.0.19041.0 - 10.0.17763.0 - AppKit.Sample.WinUI - AppKitSample.WinUI - app.manifest - ..\Shared\Brand\app.ico - x86;x64;ARM64 - win-x86;win-x64;win-arm64 - true - None - true - - $(DefineConstants);DISABLE_XAML_GENERATED_MAIN - + + + WinExe + net10.0-windows10.0.19041.0 + 10.0.17763.0 + AppKit.Sample.WinUI + AppKitSample.WinUI + app.manifest + ..\Shared\Brand\app.ico + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + true + None + true + + $(DefineConstants);DISABLE_XAML_GENERATED_MAIN + - - - - - - + + + + + + - - - - + + + + - - - + + + diff --git a/samples/AppKit.Sample.WinUI/MainWindow.xaml b/samples/AppKit.Sample.WinUI/MainWindow.xaml index d5688d8..efe302b 100644 --- a/samples/AppKit.Sample.WinUI/MainWindow.xaml +++ b/samples/AppKit.Sample.WinUI/MainWindow.xaml @@ -1,47 +1,47 @@ + x:Class="AppKit.Sample.WinUI.MainWindow" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - -