Public Access
Restructure into src/tests and add build foundation (M0)
Moves the scaffold to src/DodoSSH.Api and establishes the repo conventions the rest
of the milestones build on.
Structure:
- src/{Contracts,Crypto,Domain,Infrastructure,Api}, tests/{Contracts,Crypto,Domain}.Tests
- DodoSSH.slnx rewritten with src/ and tests/ solution folders
Build:
- Directory.Build.props centralises TFM, nullable, deterministic builds and
TreatWarningsAsErrors; Directory.Packages.props pins every version centrally
- packages.lock.json committed so CI restores in locked mode
- NuGet.config clears machine-level sources, which both fixes NU1507 under central
package management and makes restore reproducible off this machine
- Microsoft.OpenApi pinned to 2.11.0: ASP.NET Core 10.0.10 resolves 2.0.0, which is
covered by GHSA-v5pm-xwqc-g5wc (high, patched in 2.7.5)
Analyzers:
- AnalysisLevel is Recommended, not All. With warnings-as-errors, All turns opinionated
naming rules into build breaks and trains people to blanket-suppress.
- BannedSymbols.txt bans DateTime.UtcNow (TimeProvider), Guid.NewGuid (CreateVersion7),
sync-over-async, MD5/SHA1, PBKDF2 and SecureString
- CA1711/CA1724 disabled: both are .NET Framework CAS-era naming rules
- PublicApiAnalyzers on Contracts only, since that assembly is the client's real contract
API:
- weather-forecast template removed
- UseHttpsRedirection removed; TLS terminates at the reverse proxy and redirecting
behind one causes loops
- /healthz/{live,ready,startup}. Liveness deliberately checks no dependencies so a
transient database outage cannot restart the container and kill live SSH sessions.
Notes:
- No coverage collector yet. Microsoft.Testing.Extensions.CodeCoverage pulls an MTP 1.x
MSBuild extension that throws TypeLoadException against the MTP 2.3.x xunit.v3 brings.
Coverage gates are an M3 concern; revisit with an MTP 2.x-aligned version then.
Verified: dotnet build (0 warnings), 17 tests pass, format check clean, API serves
health and OpenAPI endpoints.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
# EditorConfig for DodoSSH — https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{json,yml,yaml,js,ts,css,html,axaml,xaml,csproj,props,targets,slnx}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
# Two trailing spaces are a hard line break in Markdown.
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{cmd,bat,ps1}]
|
||||
end_of_line = crlf
|
||||
|
||||
[*.cs]
|
||||
indent_size = 4
|
||||
|
||||
#### Language conventions ####
|
||||
|
||||
csharp_style_namespace_declarations = file_scoped:error
|
||||
csharp_using_directive_placement = outside_namespace:error
|
||||
csharp_style_var_for_built_in_types = false:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = false:suggestion
|
||||
csharp_prefer_braces = true:suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_separate_import_directive_groups = false
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_readonly_field = true:warning
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
|
||||
# Async methods must be suffixed Async (VSTHRD200 equivalent via naming rules below).
|
||||
dotnet_naming_rule.async_methods_end_in_async.severity = warning
|
||||
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_method
|
||||
dotnet_naming_rule.async_methods_end_in_async.style = ends_with_async
|
||||
dotnet_naming_symbols.any_async_method.applicable_kinds = method
|
||||
dotnet_naming_symbols.any_async_method.required_modifiers = async
|
||||
dotnet_naming_style.ends_with_async.required_suffix = Async
|
||||
dotnet_naming_style.ends_with_async.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_rule.interfaces_start_with_i.severity = warning
|
||||
dotnet_naming_rule.interfaces_start_with_i.symbols = any_interface
|
||||
dotnet_naming_rule.interfaces_start_with_i.style = starts_with_i
|
||||
dotnet_naming_symbols.any_interface.applicable_kinds = interface
|
||||
dotnet_naming_style.starts_with_i.required_prefix = I
|
||||
dotnet_naming_style.starts_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_rule.private_fields_are_camel_case.severity = warning
|
||||
dotnet_naming_rule.private_fields_are_camel_case.symbols = private_field
|
||||
dotnet_naming_rule.private_fields_are_camel_case.style = camel_case_style
|
||||
dotnet_naming_symbols.private_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_field.applicable_accessibilities = private
|
||||
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||
|
||||
#### Diagnostics ####
|
||||
|
||||
# Formatting violations fail the build; `dotnet format --verify-no-changes` gates CI.
|
||||
dotnet_diagnostic.IDE0055.severity = error
|
||||
|
||||
# ConfigureAwait is not meaningful in ASP.NET Core (no SynchronizationContext). It IS
|
||||
# meaningful in the Avalonia client, which re-enables CA2007 in its own .editorconfig.
|
||||
dotnet_diagnostic.CA2007.severity = none
|
||||
|
||||
# Prefer LoggerMessage source generation over ILogger extension calls — allocation-free
|
||||
# and gives structured events by construction. Warning, so it is visible but not a wall
|
||||
# during early development; raised to error once the logging pass lands in M4.
|
||||
dotnet_diagnostic.CA1848.severity = warning
|
||||
|
||||
# Exceptions carry ProblemDetails codes, not localised text.
|
||||
dotnet_diagnostic.CA1303.severity = none
|
||||
|
||||
# CA1711 reserves the suffixes Flags, Permission, Collection, Stream and friends for
|
||||
# .NET Framework CAS and BCL base types that have no bearing on this codebase. The BCL
|
||||
# itself ships BindingFlags. PermissionFlags is the clearest name for a [Flags] enum of
|
||||
# permissions, and contorting domain vocabulary to satisfy a legacy rule costs more than
|
||||
# it returns.
|
||||
dotnet_diagnostic.CA1711.severity = none
|
||||
|
||||
# CA1724 flags any type whose name collides with a BCL *namespace* (e.g. a type named
|
||||
# Permissions vs System.Security.Permissions). Namespace-qualified resolution makes this
|
||||
# a non-issue in practice and it heavily constrains domain naming.
|
||||
dotnet_diagnostic.CA1724.severity = none
|
||||
|
||||
# We use file-scoped namespaces and modern C#; these fire on deliberate style choices.
|
||||
dotnet_diagnostic.CA1812.severity = none # internal types instantiated by DI
|
||||
dotnet_diagnostic.CA1849.severity = warning # sync call in async method
|
||||
|
||||
[tests/**/*.cs]
|
||||
# Test classes are instantiated by xunit, and test data is often public static.
|
||||
dotnet_diagnostic.CA1812.severity = none
|
||||
dotnet_diagnostic.CA1034.severity = none
|
||||
|
||||
[src/DodoSSH.Infrastructure/Migrations/*.cs]
|
||||
# EF Core generates these; do not lint or format them.
|
||||
generated_code = true
|
||||
dotnet_analyzer_diagnostic.severity = none
|
||||
dotnet_diagnostic.IDE0055.severity = none
|
||||
@@ -0,0 +1,72 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# Actions are pinned to commit SHAs, not tags: a tag can be moved to point at new code,
|
||||
# which would let a compromised action run with this workflow's permissions.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
CI: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and test (ubuntu)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
global-json-file: global.json
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
# Locked mode fails if packages.lock.json does not match the project files, so a
|
||||
# dependency cannot change without the lock file change being reviewed.
|
||||
- name: restore
|
||||
run: dotnet restore DodoSSH.slnx --locked-mode
|
||||
|
||||
- name: verify formatting
|
||||
run: dotnet format DodoSSH.slnx --verify-no-changes --no-restore
|
||||
|
||||
- name: build
|
||||
run: dotnet build DodoSSH.slnx --no-restore --configuration Release
|
||||
|
||||
- name: test
|
||||
run: dotnet test DodoSSH.slnx --no-build --configuration Release
|
||||
|
||||
# Integration tests land in M1 and need Docker for Testcontainers (PostgreSQL,
|
||||
# OpenSSH). They run on this ubuntu job because macOS runners have no Docker daemon.
|
||||
|
||||
build-windows:
|
||||
name: build (windows)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
global-json-file: global.json
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
- name: restore
|
||||
run: dotnet restore DodoSSH.slnx --locked-mode
|
||||
|
||||
# Build only. Day-to-day development happens in Rider on Windows, so a
|
||||
# Windows-specific compile break must fail CI even though the tests run on Linux.
|
||||
- name: build
|
||||
run: dotnet build DodoSSH.slnx --no-restore --configuration Release
|
||||
+4
-1
@@ -4,7 +4,10 @@
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
artifacts/
|
||||
# artifacts/* rather than artifacts/ — git does not descend into an excluded directory,
|
||||
# so a negation under a directory-level ignore never matches. The committed OpenAPI
|
||||
# document and schema snapshots live here and are CI-diffed.
|
||||
artifacts/*
|
||||
!artifacts/openapi/
|
||||
!artifacts/schema/
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Banned APIs, enforced by Microsoft.CodeAnalysis.BannedApiAnalyzers (RS0030).
|
||||
# Format: <documentation-comment-id>;<message>
|
||||
# See docs/adr/ for the reasoning behind each group.
|
||||
|
||||
## Time — everything in DodoSSH is UTC and must be fakeable in tests.
|
||||
P:System.DateTime.Now;Use TimeProvider.GetUtcNow(). All DodoSSH timestamps are UTC (timestamptz) and must be injectable for tests.
|
||||
P:System.DateTime.UtcNow;Use TimeProvider.GetUtcNow() so time can be faked in tests.
|
||||
P:System.DateTime.Today;Use TimeProvider.GetUtcNow().Date.
|
||||
P:System.DateTimeOffset.Now;Use TimeProvider.GetUtcNow().
|
||||
P:System.DateTimeOffset.UtcNow;Use TimeProvider.GetUtcNow() so time can be faked in tests.
|
||||
|
||||
## Identifiers — UUIDv7 gives sortable PKs with good index locality, and clients
|
||||
## must be able to mint ids offline.
|
||||
M:System.Guid.NewGuid;Use Guid.CreateVersion7() for sortable primary keys.
|
||||
|
||||
## Randomness — anything key-, token- or nonce-adjacent must be cryptographic.
|
||||
T:System.Random;Use RandomNumberGenerator for anything security-relevant, or inject a seeded generator for tests.
|
||||
|
||||
## Sync-over-async — deadlocks under ASP.NET and stalls the Avalonia UI thread.
|
||||
P:System.Threading.Tasks.Task`1.Result;Await the task instead; .Result deadlocks and hides exceptions in an AggregateException.
|
||||
M:System.Threading.Tasks.Task.Wait;Await the task instead.
|
||||
M:System.Threading.Tasks.Task.WaitAll;Use Task.WhenAll with await.
|
||||
M:System.Threading.Tasks.Task.WaitAny;Use Task.WhenAny with await.
|
||||
M:System.Threading.Tasks.Task.GetAwaiter;Await the task directly rather than blocking on the awaiter.
|
||||
|
||||
## Encoding — must be explicit, never the ambient codepage.
|
||||
P:System.Text.Encoding.Default;Specify the encoding explicitly; Encoding.Default varies by platform.
|
||||
|
||||
## Culture-sensitive string handling is already covered by CA1304/CA1307/CA1311,
|
||||
## which AnalysisLevel=latest-All turns on. Not duplicated here.
|
||||
|
||||
## Cryptography — the client holds key material in libsodium guarded memory, and
|
||||
## MD5/SHA1 have no place in this product. Fingerprints are SHA-256.
|
||||
T:System.Security.Cryptography.MD5;Banned. SSH fingerprints are SHA-256; see docs/crypto.md.
|
||||
T:System.Security.Cryptography.SHA1;Banned. Use SHA-256 or better.
|
||||
T:System.Security.Cryptography.Rfc2898DeriveBytes;PBKDF2 is not our KDF. Use Argon2id via DodoSSH.Crypto; see docs/crypto.md.
|
||||
T:System.Security.SecureString;Deprecated and not cross-platform. Use a pooled byte[] zeroed with CryptographicOperations.ZeroMemory.
|
||||
|
||||
# Note: constant-time comparison of secrets (CryptographicOperations.FixedTimeEquals
|
||||
# over Enumerable.SequenceEqual) is enforced by a BannedSymbols.txt scoped to
|
||||
# DodoSSH.Crypto, not globally — banning SequenceEqual everywhere is pure noise in
|
||||
# business logic and tests, and noisy bans just train people to suppress them.
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Quality gates">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<!--
|
||||
Recommended, not All. With TreatWarningsAsErrors on, latest-All turns opinionated
|
||||
naming rules (CA1711 reserved suffixes, CA1724 namespace collisions) into build
|
||||
breaks, which trains people to reach for blanket suppressions and devalues the
|
||||
analyzers that catch real defects. Correctness and security rules we specifically
|
||||
want beyond Recommended are raised individually in .editorconfig.
|
||||
-->
|
||||
<AnalysisLevel>latest-Recommended</AnalysisLevel>
|
||||
<AnalysisMode>Recommended</AnalysisMode>
|
||||
<!-- The whole product is UTC-only and container-hosted; no tzdata, no culture-sensitive
|
||||
formatting. Set here rather than per-project so it cannot drift. -->
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Reproducible builds">
|
||||
<Deterministic>true</Deterministic>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<!-- packages.lock.json is committed; CI restores in locked mode. -->
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Assembly metadata">
|
||||
<Company>DodoTech</Company>
|
||||
<Product>DodoSSH</Product>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Analyzers">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
|
||||
<PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Banned API list">
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,52 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Versions are pinned here for the whole solution. Packages are added per milestone
|
||||
rather than all at once, so that every entry is one we have actually verified and
|
||||
restored. See docs/adr/ for the choices behind the notable ones.
|
||||
-->
|
||||
|
||||
<ItemGroup Label="ASP.NET Core">
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Pinned transitive dependencies">
|
||||
<!--
|
||||
Microsoft.AspNetCore.OpenApi 10.0.10 resolves Microsoft.OpenApi 2.0.0, which is
|
||||
covered by GHSA-v5pm-xwqc-g5wc (high: circular schema references can terminate
|
||||
OpenAPI parsing; vulnerable <= 2.7.4, patched in 2.7.5). Pinned forward within the
|
||||
2.x major that ASP.NET Core 10 targets. Revisit when the ASP.NET Core package
|
||||
itself moves off 2.0.0.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="2.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Analyzers">
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
|
||||
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.134" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Testing">
|
||||
<!--
|
||||
xunit.v3 runs on Microsoft.Testing.Platform, not VSTest. Microsoft.NET.Test.Sdk and
|
||||
coverlet.collector are VSTest components: referencing them alongside MTP raises
|
||||
MTP0001 and their collector never runs, so neither is referenced.
|
||||
|
||||
No coverage collector yet. Microsoft.Testing.Extensions.CodeCoverage 18.9.0 pulls
|
||||
Microsoft.Testing.Platform.MSBuild 1.9.1, which is built against MTP 1.x and throws
|
||||
TypeLoadException on IDataConsumer against the MTP 2.3.x that xunit.v3 3.2.2 brings.
|
||||
Coverage gates are an M3 concern (90% on Domain and Authorization); pick a version
|
||||
aligned with MTP 2.x then rather than carrying a broken dependency until it matters.
|
||||
-->
|
||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
||||
<PackageVersion Include="NSubstitute" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+25
-1
@@ -1,3 +1,27 @@
|
||||
<Solution>
|
||||
<Project Path="DodoSSH/DodoSSH.csproj" />
|
||||
|
||||
<Folder Name="/solution items/">
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="BannedSymbols.txt" />
|
||||
<File Path=".editorconfig" />
|
||||
<File Path="NuGet.config" />
|
||||
<File Path="global.json" />
|
||||
<File Path=".gitattributes" />
|
||||
</Folder>
|
||||
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/DodoSSH.Contracts/DodoSSH.Contracts.csproj" />
|
||||
<Project Path="src/DodoSSH.Crypto/DodoSSH.Crypto.csproj" />
|
||||
<Project Path="src/DodoSSH.Domain/DodoSSH.Domain.csproj" />
|
||||
<Project Path="src/DodoSSH.Infrastructure/DodoSSH.Infrastructure.csproj" />
|
||||
<Project Path="src/DodoSSH.Api/DodoSSH.Api.csproj" />
|
||||
</Folder>
|
||||
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/DodoSSH.Contracts.Tests/DodoSSH.Contracts.Tests.csproj" />
|
||||
<Project Path="tests/DodoSSH.Crypto.Tests/DodoSSH.Crypto.Tests.csproj" />
|
||||
<Project Path="tests/DodoSSH.Domain.Tests/DodoSSH.Domain.Tests.csproj" />
|
||||
</Folder>
|
||||
|
||||
</Solution>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
@DodoSSH_HostAddress = http://localhost:5233
|
||||
|
||||
GET {{DodoSSH_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -1,41 +0,0 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
var summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
|
||||
app.Run();
|
||||
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
|
||||
<!--
|
||||
clear drops any machine- or user-level sources so a restore here resolves the same
|
||||
packages on every developer machine and in CI. Central package management also
|
||||
requires either a single source or explicit source mapping (NU1507).
|
||||
-->
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
|
||||
<config>
|
||||
<!-- Keep the restore cache inside the repo? No: the default global cache is
|
||||
correct and shared. This section is left for future proxy settings. -->
|
||||
</config>
|
||||
|
||||
<auditSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</auditSources>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<!--
|
||||
TargetFramework, Nullable, ImplicitUsings, analyzers and central package
|
||||
management all come from ../../Directory.Build.props.
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Integration tests reach Program through WebApplicationFactory<Program>. -->
|
||||
<InternalsVisibleTo Include="DodoSSH.Api.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
using DodoSSH.Api.Setup;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddDodoOpenApi();
|
||||
builder.Services.AddDodoHealthChecks();
|
||||
|
||||
// DateTime.UtcNow is banned repo-wide (see BannedSymbols.txt); everything takes
|
||||
// TimeProvider so time can be faked in tests.
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Deliberately no UseHttpsRedirection: the API is always fronted by a reverse proxy
|
||||
// (Caddy in the reference compose stack) which terminates TLS. Redirecting here
|
||||
// produces redirect loops behind a proxy. HTTPS in development comes from the
|
||||
// launch profile instead.
|
||||
|
||||
app.MapDodoHealthChecks();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace DodoSSH.Api.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Health check wiring.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The split between liveness and readiness is deliberate and load-bearing for the
|
||||
/// relay: <c>/healthz/live</c> checks the process and nothing else, so a transient
|
||||
/// PostgreSQL outage cannot cause the orchestrator to restart the container and
|
||||
/// guillotine every live SSH session. Dependency checks belong in
|
||||
/// <c>/healthz/ready</c>, which only removes the instance from load balancing.
|
||||
/// </remarks>
|
||||
internal static class HealthChecks
|
||||
{
|
||||
/// <summary>Tag for checks that gate readiness (dependencies).</summary>
|
||||
internal const string ReadyTag = "ready";
|
||||
|
||||
/// <summary>Tag for checks that gate startup completion.</summary>
|
||||
internal const string StartupTag = "startup";
|
||||
|
||||
internal static IServiceCollection AddDodoHealthChecks(this IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks();
|
||||
|
||||
// Dependency checks are registered by the milestone that introduces the
|
||||
// dependency, each tagged ReadyTag:
|
||||
// M1 — PostgreSQL, OIDC discovery + JWKS reachability, pending migrations
|
||||
// M4 — Data Protection key ring readability
|
||||
return services;
|
||||
}
|
||||
|
||||
internal static WebApplication MapDodoHealthChecks(this WebApplication app)
|
||||
{
|
||||
// Liveness: process is running and the pipeline responds. No dependencies.
|
||||
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
|
||||
{
|
||||
Predicate = _ => false,
|
||||
}).AllowAnonymous();
|
||||
|
||||
// Readiness: safe to route traffic here.
|
||||
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
|
||||
{
|
||||
Predicate = registration => registration.Tags.Contains(ReadyTag),
|
||||
}).AllowAnonymous();
|
||||
|
||||
// Startup: one-time initialisation finished (K8s startupProbe).
|
||||
app.MapHealthChecks("/healthz/startup", new HealthCheckOptions
|
||||
{
|
||||
Predicate = registration => registration.Tags.Contains(StartupTag),
|
||||
}).AllowAnonymous();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace DodoSSH.Api.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAPI document configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The generated document exists for third parties and a future CLI. It is emitted at
|
||||
/// build time to <c>artifacts/openapi/v1.json</c> and diffed in CI so an unintended
|
||||
/// contract change fails the pull request. The desktop client's actual contract is the
|
||||
/// <c>DodoSSH.Contracts</c> assembly, guarded by PublicApiAnalyzers.
|
||||
/// </remarks>
|
||||
internal static class OpenApi
|
||||
{
|
||||
internal const string DocumentName = "v1";
|
||||
|
||||
internal static IServiceCollection AddDodoOpenApi(this IServiceCollection services)
|
||||
{
|
||||
services.AddOpenApi(DocumentName);
|
||||
|
||||
// Added in M1, once there are endpoints to describe:
|
||||
// - a document transformer contributing the OAuth2 authorizationCode + PKCE
|
||||
// security scheme, so the document is usable from a generated client
|
||||
// - a schema transformer mapping byte[] to {type: string, format: byte},
|
||||
// since every ciphertext field crosses the wire as base64
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.AspNetCore.OpenApi": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "d4Atx9IHq7JgX0F/h7Db+m9zAUzC+cKdI9k+OWnnyQIOUQtfvjIEuhvbjPigVMkAmPUgCbJ8Yp6M9ghUqHtJSQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.OpenApi": "2.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"Microsoft.OpenApi": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.11.0, )",
|
||||
"resolved": "2.11.0",
|
||||
"contentHash": "/ignjfdeKT2SGLIR7QEv19KnI0rvoxRG/TYDOZdK9EsWLjKK9IK8i1Mo5NRm9PRV3i64DzlTqnIflWvoyfljLg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
DTOs shared between the API and the desktop client. This assembly, not the generated
|
||||
OpenAPI document, is the real client contract, so PublicApiAnalyzers is enabled here
|
||||
and only here: an accidental change to a public member becomes a build error rather
|
||||
than a runtime deserialisation failure on somebody's laptop.
|
||||
|
||||
Track additions in PublicAPI.Unshipped.txt; move them to PublicAPI.Shipped.txt when a
|
||||
version is released.
|
||||
-->
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="PublicAPI.Shipped.txt" />
|
||||
<AdditionalFiles Include="PublicAPI.Unshipped.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace DodoSSH.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Stable machine-readable error codes returned in the <c>code</c> extension of an
|
||||
/// RFC 9457 ProblemDetails response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These live in Contracts so the client switches on constants rather than parsing prose.
|
||||
/// The values are part of the public contract: add freely, never rename or repurpose.
|
||||
/// </remarks>
|
||||
public static class ProblemCodes
|
||||
{
|
||||
/// <summary>The base URI that every problem <c>type</c> is formed under.</summary>
|
||||
public const string TypeBaseUri = "https://dodossh.dev/problems/";
|
||||
|
||||
/// <summary>A push operation's <c>expectedVersion</c> did not match the stored row.</summary>
|
||||
public const string VaultConflict = "vault-conflict";
|
||||
|
||||
/// <summary>The caller is authenticated but lacks the required permission.</summary>
|
||||
public const string Forbidden = "forbidden";
|
||||
|
||||
/// <summary>The sync cursor was malformed, or failed its integrity tag.</summary>
|
||||
public const string InvalidCursor = "invalid-cursor";
|
||||
|
||||
/// <summary>An <c>Idempotency-Key</c> was reused with a different request body.</summary>
|
||||
public const string IdempotencyKeyReuse = "idempotency-key-reuse";
|
||||
|
||||
/// <summary>The caller has not yet enrolled a public key, so no vault is reachable.</summary>
|
||||
public const string EnrollmentRequired = "enrollment-required";
|
||||
|
||||
/// <summary>Enrollment was attempted for a user who already holds a current key.</summary>
|
||||
public const string AlreadyEnrolled = "already-enrolled";
|
||||
|
||||
/// <summary>The relay refused the requested target. Never states why, to avoid a probe oracle.</summary>
|
||||
public const string RelayTargetRejected = "relay-target-rejected";
|
||||
|
||||
/// <summary>The relay ticket is expired, already used, or not valid for this node.</summary>
|
||||
public const string RelayTicketInvalid = "relay-ticket-invalid";
|
||||
|
||||
/// <summary>A per-user or per-node relay session limit was reached.</summary>
|
||||
public const string RelayLimitReached = "relay-limit-reached";
|
||||
|
||||
/// <summary>The client is older than the server's <c>minClientVersion</c>.</summary>
|
||||
public const string ClientTooOld = "client-too-old";
|
||||
|
||||
/// <summary>A push batch exceeded the operation count or payload size cap.</summary>
|
||||
public const string PushBatchTooLarge = "push-batch-too-large";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#nullable enable
|
||||
@@ -0,0 +1,14 @@
|
||||
#nullable enable
|
||||
DodoSSH.Contracts.ProblemCodes
|
||||
const DodoSSH.Contracts.ProblemCodes.AlreadyEnrolled = "already-enrolled" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.ClientTooOld = "client-too-old" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.EnrollmentRequired = "enrollment-required" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.Forbidden = "forbidden" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.IdempotencyKeyReuse = "idempotency-key-reuse" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.InvalidCursor = "invalid-cursor" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.PushBatchTooLarge = "push-batch-too-large" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.RelayLimitReached = "relay-limit-reached" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.RelayTargetRejected = "relay-target-rejected" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.RelayTicketInvalid = "relay-ticket-invalid" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.TypeBaseUri = "https://dodossh.dev/problems/" -> string!
|
||||
const DodoSSH.Contracts.ProblemCodes.VaultConflict = "vault-conflict" -> string!
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ=="
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace DodoSSH.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Constants of the DodoSSH cryptographic specification.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// docs/crypto.md is the normative specification; this type must agree with it exactly.
|
||||
/// The implementation of the envelope, AAD derivation and key wrapping lands in M1, once
|
||||
/// the specification and its test vectors are frozen. Nothing else may be built on top of
|
||||
/// an unfrozen AAD: only clients can re-encrypt, so a change after users hold data cannot
|
||||
/// be migrated server-side.
|
||||
/// </remarks>
|
||||
public static class CryptoSpec
|
||||
{
|
||||
/// <summary>Magic prefix identifying a DSH1 envelope.</summary>
|
||||
public const string EnvelopeMagic = "DSH1";
|
||||
|
||||
/// <summary>Version of the AAD derivation rule that payloads are bound to.</summary>
|
||||
/// <remarks>
|
||||
/// Stored per row as <c>payload_aad_version</c> so a future change can be applied
|
||||
/// lazily, re-encrypting on next write rather than in a migration.
|
||||
/// </remarks>
|
||||
public const short CurrentAadVersion = 1;
|
||||
|
||||
/// <summary>Domain-separation prefix for every AAD computation.</summary>
|
||||
public const string AadDomainPrefix = "dsh1\n";
|
||||
|
||||
/// <summary>Identifiers for the algorithms an envelope may declare.</summary>
|
||||
public enum AlgorithmId : byte
|
||||
{
|
||||
/// <summary>Reserved; never written.</summary>
|
||||
Unspecified = 0,
|
||||
|
||||
/// <summary>Symmetric content encryption under a known key.</summary>
|
||||
XChaCha20Poly1305 = 1,
|
||||
|
||||
/// <summary>Symmetric fallback where XChaCha20 is unavailable.</summary>
|
||||
Aes256Gcm = 2,
|
||||
|
||||
/// <summary>Anonymous-sender seal to an X25519 public key.</summary>
|
||||
SealToX25519 = 3,
|
||||
|
||||
// 4 is reserved for a hybrid X25519 + ML-KEM-768 seal. Store-now-decrypt-later is
|
||||
// a genuine threat for long-lived SSH keys, so the identifier is claimed now even
|
||||
// though the construction ships later.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
The DSH1 envelope format, AAD derivation, key wrapping and the KDF.
|
||||
Referenced by both the API and the client, but the server only ever uses the format
|
||||
and fingerprint constants: it never holds a key and never decrypts a payload.
|
||||
See docs/crypto.md, which is the normative specification for everything here.
|
||||
-->
|
||||
|
||||
<PropertyGroup>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DodoSSH.Crypto.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace DodoSSH.Domain.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// PermissionFlags a subject (user or team) may hold over a vault or an individual resource.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Evaluation is a plain union across the subject's direct grants and the grants held by
|
||||
/// teams they belong to. There are deliberately no Deny rules: union-only evaluation is
|
||||
/// monotonic and straightforward to test, and Deny can be added later additively if a
|
||||
/// real need appears. Express restriction by granting narrowly instead.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Connect"/> is a user-interface hint, <b>not</b> a security boundary. SSH
|
||||
/// terminates on the client, so opening a session requires the credential's plaintext on
|
||||
/// that machine; "may connect but may not view the key" is therefore unenforceable in
|
||||
/// this architecture. Treat it as an anti-shoulder-surfing convenience and never document
|
||||
/// it as access control. See docs/adr/0001-e2ee-trust-model.md.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Flags]
|
||||
public enum PermissionFlags
|
||||
{
|
||||
/// <summary>No access.</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>May fetch and decrypt the resource. Implies the ability to use it.</summary>
|
||||
Read = 1 << 0,
|
||||
|
||||
/// <summary>May create, modify and soft-delete resources in the vault.</summary>
|
||||
Write = 1 << 1,
|
||||
|
||||
/// <summary>Intent hint that the subject uses this host for sessions. Not a boundary.</summary>
|
||||
Connect = 1 << 2,
|
||||
|
||||
/// <summary>May grant access to other subjects, which requires re-wrapping the vault key.</summary>
|
||||
Share = 1 << 3,
|
||||
|
||||
/// <summary>May administer the vault itself: rename, rekey, manage ACLs.</summary>
|
||||
Admin = 1 << 4,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Entities, enums and invariants. No EF Core reference: persistence concerns live in
|
||||
DodoSSH.Infrastructure so the domain stays unit-testable with no database.
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DodoSSH.Domain.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Persistence: DodoDbContext, IEntityTypeConfiguration implementations, migrations and
|
||||
query helpers. EF Core and Npgsql arrive in M1 with the first migration.
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../DodoSSH.Domain/DodoSSH.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DodoSSH.Infrastructure.Tests" />
|
||||
<InternalsVisibleTo Include="DodoSSH.Api.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"dodossh.domain": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project>
|
||||
|
||||
<!-- Inherit everything from the repo root, then relax and add what test projects need. -->
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<!-- Test projects legitimately do things production code should not: assert on
|
||||
nulls, construct throwaway objects, use reflection. Keep warnings visible
|
||||
but non-fatal so a test file never blocks a build. -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<!-- xunit.v3 runs on Microsoft.Testing.Platform, which needs an executable host. -->
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
|
||||
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Analyzer relaxations for tests">
|
||||
<!-- CA1707: test method names use underscores by convention (Method_State_Expectation).
|
||||
CA2007: no SynchronizationContext in tests, ConfigureAwait is noise.
|
||||
CA1861: inline constant arrays in theories are clearer than static fields. -->
|
||||
<NoWarn>$(NoWarn);CA1707;CA2007;CA1861;CA1052;CA1515</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Test framework — every test project gets these">
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="Shouldly" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="Shouldly" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Grows into the shared server/client contract suite: snapshot tests over every
|
||||
serialised DTO, and the tests/fixtures/sync/*.json golden files that both this suite
|
||||
and the client's sync tests consume. Those fixtures, not the OpenAPI document, are
|
||||
the contract test.
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/DodoSSH.Contracts/DodoSSH.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Reflection;
|
||||
using DodoSSH.Contracts;
|
||||
|
||||
namespace DodoSSH.Contracts.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Problem codes are part of the public wire contract: the client switches on them.
|
||||
/// </summary>
|
||||
public sealed class ProblemCodesTests
|
||||
{
|
||||
private static IReadOnlyList<FieldInfo> CodeFields =>
|
||||
[.. typeof(ProblemCodes)
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(f => f.IsLiteral && f.FieldType == typeof(string))
|
||||
.Where(f => !string.Equals(f.Name, nameof(ProblemCodes.TypeBaseUri), StringComparison.Ordinal))];
|
||||
|
||||
[Fact]
|
||||
public void AllCodes_AreUnique()
|
||||
{
|
||||
// A duplicated value would make two distinct failures indistinguishable to the
|
||||
// client, which is the whole point of having codes rather than parsing messages.
|
||||
var values = CodeFields.Select(f => (string)f.GetRawConstantValue()!).ToList();
|
||||
|
||||
values.Distinct(StringComparer.Ordinal).Count().ShouldBe(values.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllCodes_AreKebabCase()
|
||||
{
|
||||
// Codes are appended to TypeBaseUri to form the ProblemDetails type URI, so they
|
||||
// must be URL-safe and stylistically consistent.
|
||||
foreach (var field in CodeFields)
|
||||
{
|
||||
var value = (string)field.GetRawConstantValue()!;
|
||||
|
||||
value.ShouldMatch("^[a-z][a-z0-9]*(-[a-z0-9]+)*$", $"{field.Name} is not kebab-case");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeBaseUri_IsAnAbsoluteHttpsUriEndingInSlash()
|
||||
{
|
||||
Uri.TryCreate(ProblemCodes.TypeBaseUri, UriKind.Absolute, out var uri).ShouldBeTrue();
|
||||
uri!.Scheme.ShouldBe(Uri.UriSchemeHttps);
|
||||
ProblemCodes.TypeBaseUri.ShouldEndWith("/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"NSubstitute": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==",
|
||||
"dependencies": {
|
||||
"Castle.Core": "5.1.1"
|
||||
}
|
||||
},
|
||||
"Shouldly": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.3.0, )",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
|
||||
"dependencies": {
|
||||
"DiffEngine": "11.3.0",
|
||||
"EmptyFiles": "4.4.0"
|
||||
}
|
||||
},
|
||||
"xunit.v3": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.2.2, )",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==",
|
||||
"dependencies": {
|
||||
"xunit.v3.mtp-v1": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"Castle.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.1.1",
|
||||
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "6.0.0"
|
||||
}
|
||||
},
|
||||
"DiffEngine": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.0",
|
||||
"contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
|
||||
"dependencies": {
|
||||
"EmptyFiles": "4.4.0",
|
||||
"System.Management": "6.0.1"
|
||||
}
|
||||
},
|
||||
"EmptyFiles": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
|
||||
},
|
||||
"Microsoft.ApplicationInsights": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
|
||||
},
|
||||
"Microsoft.Testing.Extensions.Telemetry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.ApplicationInsights": "2.23.0",
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Testing.Platform": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA=="
|
||||
},
|
||||
"Microsoft.Testing.Platform.MSBuild": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.Registry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg=="
|
||||
},
|
||||
"System.CodeDom": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
||||
},
|
||||
"System.Diagnostics.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
|
||||
},
|
||||
"System.Management": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.1",
|
||||
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
|
||||
"dependencies": {
|
||||
"System.CodeDom": "6.0.0"
|
||||
}
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.27.0",
|
||||
"contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g=="
|
||||
},
|
||||
"xunit.v3.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA=="
|
||||
},
|
||||
"xunit.v3.common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0"
|
||||
}
|
||||
},
|
||||
"xunit.v3.core.mtp-v1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Extensions.Telemetry": "1.9.1",
|
||||
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1",
|
||||
"Microsoft.Testing.Platform": "1.9.1",
|
||||
"Microsoft.Testing.Platform.MSBuild": "1.9.1",
|
||||
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||
"xunit.v3.runner.inproc.console": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==",
|
||||
"dependencies": {
|
||||
"xunit.v3.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.mtp-v1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.27.0",
|
||||
"xunit.v3.assert": "[3.2.2]",
|
||||
"xunit.v3.core.mtp-v1": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.runner.common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.Registry": "[5.0.0]",
|
||||
"xunit.v3.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.runner.inproc.console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==",
|
||||
"dependencies": {
|
||||
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||
"xunit.v3.runner.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"dodossh.contracts": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using DodoSSH.Crypto;
|
||||
|
||||
namespace DodoSSH.Crypto.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the specification constants that are written into stored data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These are not busywork. The envelope magic and AAD version are persisted in every
|
||||
/// ciphertext row, and only clients can re-encrypt: if one of these changes without a
|
||||
/// deliberate migration path, existing vaults stop decrypting and the server cannot help.
|
||||
/// </remarks>
|
||||
public sealed class CryptoSpecTests
|
||||
{
|
||||
[Fact]
|
||||
public void EnvelopeMagic_IsStable()
|
||||
{
|
||||
CryptoSpec.EnvelopeMagic.ShouldBe("DSH1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentAadVersion_IsStable()
|
||||
{
|
||||
// Bumping this requires a lazy re-encrypt-on-write path in the client first.
|
||||
CryptoSpec.CurrentAadVersion.ShouldBe((short)1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CryptoSpec.AlgorithmId.XChaCha20Poly1305, 1)]
|
||||
[InlineData(CryptoSpec.AlgorithmId.Aes256Gcm, 2)]
|
||||
[InlineData(CryptoSpec.AlgorithmId.SealToX25519, 3)]
|
||||
public void AlgorithmId_HasStableWireValue(CryptoSpec.AlgorithmId algorithm, int expected)
|
||||
{
|
||||
((int)algorithm).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlgorithmId_4_IsReservedForHybridPostQuantumSeal()
|
||||
{
|
||||
// Reserved for X25519 + ML-KEM-768. Claimed now so the identifier cannot be
|
||||
// reused: store-now-decrypt-later is a real threat for long-lived SSH keys.
|
||||
// AlgorithmId is byte-backed, matching the single alg_id byte in the envelope.
|
||||
Enum.IsDefined(typeof(CryptoSpec.AlgorithmId), (byte)4).ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Once docs/crypto.md is frozen in M1 this project gains, in order of importance:
|
||||
- frozen golden-vector tests over the DSH1 envelope, so a release can never
|
||||
silently change the format and brick every existing vault
|
||||
- AAD derivation vectors, including the negative cases that prove a payload cannot
|
||||
be moved between rows, resources or key generations
|
||||
- differential tests running each primitive through both NSec and BouncyCastle
|
||||
- round-trip property tests
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/DodoSSH.Crypto/DodoSSH.Crypto.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"NSubstitute": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==",
|
||||
"dependencies": {
|
||||
"Castle.Core": "5.1.1"
|
||||
}
|
||||
},
|
||||
"Shouldly": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.3.0, )",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
|
||||
"dependencies": {
|
||||
"DiffEngine": "11.3.0",
|
||||
"EmptyFiles": "4.4.0"
|
||||
}
|
||||
},
|
||||
"xunit.v3": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.2.2, )",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==",
|
||||
"dependencies": {
|
||||
"xunit.v3.mtp-v1": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"Castle.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.1.1",
|
||||
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "6.0.0"
|
||||
}
|
||||
},
|
||||
"DiffEngine": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.0",
|
||||
"contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
|
||||
"dependencies": {
|
||||
"EmptyFiles": "4.4.0",
|
||||
"System.Management": "6.0.1"
|
||||
}
|
||||
},
|
||||
"EmptyFiles": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
|
||||
},
|
||||
"Microsoft.ApplicationInsights": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
|
||||
},
|
||||
"Microsoft.Testing.Extensions.Telemetry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.ApplicationInsights": "2.23.0",
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Testing.Platform": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA=="
|
||||
},
|
||||
"Microsoft.Testing.Platform.MSBuild": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.Registry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg=="
|
||||
},
|
||||
"System.CodeDom": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
||||
},
|
||||
"System.Diagnostics.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
|
||||
},
|
||||
"System.Management": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.1",
|
||||
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
|
||||
"dependencies": {
|
||||
"System.CodeDom": "6.0.0"
|
||||
}
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.27.0",
|
||||
"contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g=="
|
||||
},
|
||||
"xunit.v3.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA=="
|
||||
},
|
||||
"xunit.v3.common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0"
|
||||
}
|
||||
},
|
||||
"xunit.v3.core.mtp-v1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Extensions.Telemetry": "1.9.1",
|
||||
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1",
|
||||
"Microsoft.Testing.Platform": "1.9.1",
|
||||
"Microsoft.Testing.Platform.MSBuild": "1.9.1",
|
||||
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||
"xunit.v3.runner.inproc.console": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==",
|
||||
"dependencies": {
|
||||
"xunit.v3.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.mtp-v1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.27.0",
|
||||
"xunit.v3.assert": "[3.2.2]",
|
||||
"xunit.v3.core.mtp-v1": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.runner.common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.Registry": "[5.0.0]",
|
||||
"xunit.v3.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.runner.inproc.console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==",
|
||||
"dependencies": {
|
||||
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||
"xunit.v3.runner.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"dodossh.crypto": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using DodoSSH.Domain.Authorization;
|
||||
|
||||
namespace DodoSSH.Domain.Tests.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Permission algebra. This grows into the full effective-permission suite in M3; for now
|
||||
/// it pins the flag values, because they are persisted as an integer column and changing
|
||||
/// one silently reinterprets every stored ACL row.
|
||||
/// </summary>
|
||||
public sealed class PermissionFlagsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(PermissionFlags.None, 0)]
|
||||
[InlineData(PermissionFlags.Read, 1)]
|
||||
[InlineData(PermissionFlags.Write, 2)]
|
||||
[InlineData(PermissionFlags.Connect, 4)]
|
||||
[InlineData(PermissionFlags.Share, 8)]
|
||||
[InlineData(PermissionFlags.Admin, 16)]
|
||||
public void Flag_HasStableWireValue(PermissionFlags flag, int expected)
|
||||
{
|
||||
// These values are persisted; a change reinterprets existing ACL rows.
|
||||
((int)flag).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Union_CombinesGrantsFromMultipleSubjects()
|
||||
{
|
||||
// Effective permissions are a union across direct and team-derived grants.
|
||||
var direct = PermissionFlags.Read;
|
||||
var viaTeam = PermissionFlags.Write | PermissionFlags.Connect;
|
||||
|
||||
var effective = direct | viaTeam;
|
||||
|
||||
effective.HasFlag(PermissionFlags.Read).ShouldBeTrue();
|
||||
effective.HasFlag(PermissionFlags.Write).ShouldBeTrue();
|
||||
effective.HasFlag(PermissionFlags.Connect).ShouldBeTrue();
|
||||
effective.HasFlag(PermissionFlags.Admin).ShouldBeFalse();
|
||||
effective.HasFlag(PermissionFlags.Share).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Union_IsMonotonic()
|
||||
{
|
||||
// The deliberate consequence of having no Deny rules: adding a grant can only
|
||||
// ever widen access, never narrow it. M3's evaluator relies on this.
|
||||
var before = PermissionFlags.Read;
|
||||
var after = before | PermissionFlags.Admin;
|
||||
|
||||
(after & before).ShouldBe(before);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Test framework packages come from ../Directory.Build.props. -->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/DodoSSH.Domain/DodoSSH.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Meziantou.Analyzer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.134, )",
|
||||
"resolved": "3.0.134",
|
||||
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.0, )",
|
||||
"resolved": "5.6.0",
|
||||
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||
},
|
||||
"NSubstitute": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==",
|
||||
"dependencies": {
|
||||
"Castle.Core": "5.1.1"
|
||||
}
|
||||
},
|
||||
"Shouldly": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.3.0, )",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
|
||||
"dependencies": {
|
||||
"DiffEngine": "11.3.0",
|
||||
"EmptyFiles": "4.4.0"
|
||||
}
|
||||
},
|
||||
"xunit.v3": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.2.2, )",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==",
|
||||
"dependencies": {
|
||||
"xunit.v3.mtp-v1": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"Castle.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.1.1",
|
||||
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "6.0.0"
|
||||
}
|
||||
},
|
||||
"DiffEngine": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.0",
|
||||
"contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
|
||||
"dependencies": {
|
||||
"EmptyFiles": "4.4.0",
|
||||
"System.Management": "6.0.1"
|
||||
}
|
||||
},
|
||||
"EmptyFiles": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
|
||||
},
|
||||
"Microsoft.ApplicationInsights": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
|
||||
},
|
||||
"Microsoft.Testing.Extensions.Telemetry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.ApplicationInsights": "2.23.0",
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Testing.Platform": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA=="
|
||||
},
|
||||
"Microsoft.Testing.Platform.MSBuild": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.9.1",
|
||||
"contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Platform": "1.9.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.Registry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg=="
|
||||
},
|
||||
"System.CodeDom": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
||||
},
|
||||
"System.Diagnostics.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
|
||||
},
|
||||
"System.Management": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.1",
|
||||
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
|
||||
"dependencies": {
|
||||
"System.CodeDom": "6.0.0"
|
||||
}
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.27.0",
|
||||
"contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g=="
|
||||
},
|
||||
"xunit.v3.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA=="
|
||||
},
|
||||
"xunit.v3.common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0"
|
||||
}
|
||||
},
|
||||
"xunit.v3.core.mtp-v1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Testing.Extensions.Telemetry": "1.9.1",
|
||||
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1",
|
||||
"Microsoft.Testing.Platform": "1.9.1",
|
||||
"Microsoft.Testing.Platform.MSBuild": "1.9.1",
|
||||
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||
"xunit.v3.runner.inproc.console": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==",
|
||||
"dependencies": {
|
||||
"xunit.v3.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.mtp-v1": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.27.0",
|
||||
"xunit.v3.assert": "[3.2.2]",
|
||||
"xunit.v3.core.mtp-v1": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.runner.common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.Registry": "[5.0.0]",
|
||||
"xunit.v3.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"xunit.v3.runner.inproc.console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.2",
|
||||
"contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==",
|
||||
"dependencies": {
|
||||
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||
"xunit.v3.runner.common": "[3.2.2]"
|
||||
}
|
||||
},
|
||||
"dodossh.domain": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user