Add PHP Analyzer Plugin and Composer Lock Data Handling
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented the PhpAnalyzerPlugin to analyze PHP projects. - Created ComposerLockData class to represent data from composer.lock files. - Developed ComposerLockReader to load and parse composer.lock files asynchronously. - Introduced ComposerPackage class to encapsulate package details. - Added PhpPackage class to represent PHP packages with metadata and evidence. - Implemented PhpPackageCollector to gather packages from ComposerLockData. - Created PhpLanguageAnalyzer to perform analysis and emit results. - Added capability signals for known PHP frameworks and CMS. - Developed unit tests for the PHP language analyzer and its components. - Included sample composer.lock and expected output for testing. - Updated project files for the new PHP analyzer library and tests.
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Zastava.Core.Configuration;
|
||||
using StellaOps.Zastava.Observer.Backend;
|
||||
using StellaOps.Zastava.Observer.Configuration;
|
||||
using StellaOps.Zastava.Observer.ContainerRuntime.Cri;
|
||||
using StellaOps.Zastava.Observer.ContainerRuntime;
|
||||
using StellaOps.Zastava.Observer.ContainerRuntime.Cri;
|
||||
using StellaOps.Zastava.Observer.Posture;
|
||||
using StellaOps.Zastava.Observer.Runtime;
|
||||
using StellaOps.Zastava.Observer.Secrets;
|
||||
using StellaOps.Zastava.Observer.Surface;
|
||||
using StellaOps.Zastava.Observer.Worker;
|
||||
using StellaOps.Zastava.Observer.Backend;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -83,6 +89,35 @@ public static class ObserverServiceCollectionExtensions
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<ZastavaRuntimeOptions>, ObserverRuntimeOptionsPostConfigure>());
|
||||
|
||||
// Surface environment + cache/manifest/secrets wiring
|
||||
services.AddSurfaceEnvironment(options =>
|
||||
{
|
||||
options.ComponentName = "Zastava.Observer";
|
||||
options.AddPrefix("ZASTAVA_OBSERVER");
|
||||
options.AddPrefix("ZASTAVA");
|
||||
options.KnownFeatureFlags.Add("drift");
|
||||
options.KnownFeatureFlags.Add("prefetch");
|
||||
options.TenantResolver = sp => sp.GetRequiredService<IOptions<ZastavaRuntimeOptions>>().Value.Tenant;
|
||||
});
|
||||
|
||||
services.AddSurfaceFileCache();
|
||||
services.AddSurfaceManifestStore();
|
||||
services.AddSurfaceSecrets(options =>
|
||||
{
|
||||
options.ComponentName = "Zastava.Observer";
|
||||
options.RequiredSecretTypes.Add("cas-access");
|
||||
options.RequiredSecretTypes.Add("attestation");
|
||||
});
|
||||
|
||||
services.TryAddSingleton(sp => sp.GetRequiredService<ISurfaceEnvironment>().Settings);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceCacheOptions>>(sp =>
|
||||
new SurfaceCacheOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceManifestStoreOptions>>(sp =>
|
||||
new SurfaceManifestStoreOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
|
||||
services.TryAddSingleton<IObserverSurfaceSecrets, ObserverSurfaceSecrets>();
|
||||
services.TryAddSingleton<IRuntimeSurfaceFsClient, RuntimeSurfaceFsClient>();
|
||||
|
||||
services.AddHostedService<ObserverBootstrapService>();
|
||||
services.AddHostedService<ContainerLifecycleHostedService>();
|
||||
services.AddHostedService<RuntimeEventDispatchService>();
|
||||
@@ -101,3 +136,34 @@ internal sealed class ObserverRuntimeOptionsPostConfigure : IPostConfigureOption
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SurfaceCacheOptionsConfigurator : IConfigureOptions<SurfaceCacheOptions>
|
||||
{
|
||||
private readonly SurfaceEnvironmentSettings settings;
|
||||
|
||||
public SurfaceCacheOptionsConfigurator(SurfaceEnvironmentSettings settings)
|
||||
{
|
||||
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public void Configure(SurfaceCacheOptions options)
|
||||
{
|
||||
options.RootDirectory ??= settings.CacheRoot.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SurfaceManifestStoreOptionsConfigurator : IConfigureOptions<SurfaceManifestStoreOptions>
|
||||
{
|
||||
private readonly SurfaceEnvironmentSettings settings;
|
||||
|
||||
public SurfaceManifestStoreOptionsConfigurator(SurfaceEnvironmentSettings settings)
|
||||
{
|
||||
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public void Configure(SurfaceManifestStoreOptions options)
|
||||
{
|
||||
options.Bucket = string.IsNullOrWhiteSpace(settings.SurfaceFsBucket) ? options.Bucket : settings.SurfaceFsBucket;
|
||||
options.RootDirectory ??= Path.Combine(settings.CacheRoot.FullName, "manifests");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos/runtime/v1/runtime.proto" GrpcServices="Client" />
|
||||
|
||||
@@ -19,14 +19,26 @@ internal sealed class RuntimeSurfaceFsClient : IRuntimeSurfaceFsClient
|
||||
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
||||
}
|
||||
|
||||
public Task<SurfaceManifestDocument?> TryGetManifestAsync(string manifestDigest, CancellationToken cancellationToken = default)
|
||||
public async Task<SurfaceManifestDocument?> TryGetManifestAsync(string manifestDigest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(manifestDigest))
|
||||
{
|
||||
return Task.FromResult<SurfaceManifestDocument?>(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
// manifest digests follow sha256:<hex>; manifest reader handles validation and tenant discovery
|
||||
return _manifestReader.TryGetByDigestAsync(manifestDigest.Trim(), cancellationToken);
|
||||
var manifest = await _manifestReader.TryGetByDigestAsync(manifestDigest.Trim(), cancellationToken).ConfigureAwait(false);
|
||||
if (manifest is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Enforce tenant scoping to avoid crossing cache boundaries in multi-tenant deployments.
|
||||
if (!string.IsNullOrWhiteSpace(manifest.Tenant)
|
||||
&& !string.Equals(manifest.Tenant, _environment.Tenant, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ internal sealed class AdmissionResponseBuilder
|
||||
metadata["cache"] = "hit";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(decision.SurfacePointer))
|
||||
{
|
||||
metadata["surfacePointer"] = decision.SurfacePointer!;
|
||||
}
|
||||
|
||||
var resolved = decision.ResolvedDigest ?? decision.OriginalImage;
|
||||
|
||||
images.Add(new AdmissionImageVerdict
|
||||
|
||||
@@ -2,10 +2,12 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
using StellaOps.Zastava.Core.Diagnostics;
|
||||
using StellaOps.Zastava.Webhook.Backend;
|
||||
using StellaOps.Zastava.Webhook.Configuration;
|
||||
using StellaOps.Zastava.Webhook.Surface;
|
||||
|
||||
namespace StellaOps.Zastava.Webhook.Admission;
|
||||
|
||||
@@ -22,6 +24,8 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
private readonly IOptionsMonitor<ZastavaWebhookOptions> options;
|
||||
private readonly IZastavaRuntimeMetrics runtimeMetrics;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IWebhookSurfaceFsClient surfaceFsClient;
|
||||
private readonly SurfaceEnvironmentSettings surfaceSettings;
|
||||
private readonly ILogger<RuntimeAdmissionPolicyService> logger;
|
||||
|
||||
public RuntimeAdmissionPolicyService(
|
||||
@@ -31,6 +35,8 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
IOptionsMonitor<ZastavaWebhookOptions> options,
|
||||
IZastavaRuntimeMetrics runtimeMetrics,
|
||||
TimeProvider timeProvider,
|
||||
IWebhookSurfaceFsClient surfaceFsClient,
|
||||
SurfaceEnvironmentSettings surfaceSettings,
|
||||
ILogger<RuntimeAdmissionPolicyService> logger)
|
||||
{
|
||||
this.policyClient = policyClient ?? throw new ArgumentNullException(nameof(policyClient));
|
||||
@@ -39,6 +45,8 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
this.options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
this.runtimeMetrics = runtimeMetrics ?? throw new ArgumentNullException(nameof(runtimeMetrics));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.surfaceFsClient = surfaceFsClient ?? throw new ArgumentNullException(nameof(surfaceFsClient));
|
||||
this.surfaceSettings = surfaceSettings ?? throw new ArgumentNullException(nameof(surfaceSettings));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -67,9 +75,25 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
var combinedResults = new Dictionary<string, RuntimePolicyImageResult>(StringComparer.Ordinal);
|
||||
var backendMisses = new List<string>();
|
||||
var fromCache = new HashSet<string>(StringComparer.Ordinal);
|
||||
var missingSurface = new HashSet<string>(StringComparer.Ordinal);
|
||||
var surfacePointers = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
var enforceSurface = surfaceSettings.FeatureFlags.Count == 0
|
||||
|| surfaceSettings.FeatureFlags.Contains("admission", StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var digest in resolved)
|
||||
{
|
||||
if (enforceSurface)
|
||||
{
|
||||
var (found, pointer) = await surfaceFsClient.TryGetManifestAsync(digest.Digest, cancellationToken).ConfigureAwait(false);
|
||||
if (!found)
|
||||
{
|
||||
missingSurface.Add(digest.Digest);
|
||||
continue;
|
||||
}
|
||||
|
||||
surfacePointers[digest.Digest] = pointer;
|
||||
}
|
||||
|
||||
if (cache.TryGet(digest.Digest, out var cached))
|
||||
{
|
||||
combinedResults[digest.Digest] = cached;
|
||||
@@ -134,15 +158,16 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
ResolvedDigest = resolution.ResolvedDigest,
|
||||
Verdict = allowed ? PolicyVerdict.Warn : PolicyVerdict.Error,
|
||||
Allowed = allowed,
|
||||
Policy = null,
|
||||
Reasons = reasons,
|
||||
FromCache = false,
|
||||
ResolutionFailed = false
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
decisions.Add(CreateResolutionFailureDecision(resolution));
|
||||
Policy = null,
|
||||
Reasons = reasons,
|
||||
FromCache = false,
|
||||
ResolutionFailed = false,
|
||||
SurfacePointer = surfacePointers.GetValueOrDefault(resolution.ResolvedDigest)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
decisions.Add(CreateResolutionFailureDecision(resolution));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +191,25 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
continue;
|
||||
}
|
||||
|
||||
if (missingSurface.Contains(resolution.ResolvedDigest))
|
||||
{
|
||||
var manifestDecision = new RuntimeAdmissionDecision
|
||||
{
|
||||
OriginalImage = resolution.Original,
|
||||
ResolvedDigest = resolution.ResolvedDigest,
|
||||
Verdict = PolicyVerdict.Error,
|
||||
Allowed = false,
|
||||
Policy = null,
|
||||
Reasons = new[] { "surface.manifest.missing" },
|
||||
FromCache = false,
|
||||
ResolutionFailed = false,
|
||||
SurfacePointer = null
|
||||
};
|
||||
RecordDecisionMetrics(false, false, false, RuntimeEventKind.ContainerStart);
|
||||
decisions.Add(manifestDecision);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!combinedResults.TryGetValue(resolution.ResolvedDigest, out var policyResult))
|
||||
{
|
||||
var synthetic = new RuntimeAdmissionDecision
|
||||
@@ -177,7 +221,8 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
Policy = null,
|
||||
Reasons = new[] { "zastava.policy.result.missing" },
|
||||
FromCache = false,
|
||||
ResolutionFailed = false
|
||||
ResolutionFailed = false,
|
||||
SurfacePointer = surfacePointers.GetValueOrDefault(resolution.ResolvedDigest)
|
||||
};
|
||||
RecordDecisionMetrics(false, false, false, RuntimeEventKind.ContainerStart);
|
||||
decisions.Add(synthetic);
|
||||
@@ -197,7 +242,8 @@ internal sealed class RuntimeAdmissionPolicyService : IRuntimeAdmissionPolicySer
|
||||
Policy = policyResult,
|
||||
Reasons = reasons,
|
||||
FromCache = cached,
|
||||
ResolutionFailed = false
|
||||
ResolutionFailed = false,
|
||||
SurfacePointer = surfacePointers.GetValueOrDefault(resolution.ResolvedDigest)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,6 +336,7 @@ internal sealed record RuntimeAdmissionDecision
|
||||
public IReadOnlyList<string> Reasons { get; init; } = Array.Empty<string>();
|
||||
public bool FromCache { get; init; }
|
||||
public bool ResolutionFailed { get; init; }
|
||||
public string? SurfacePointer { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record RuntimeAdmissionEvaluation
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Zastava.Core.Configuration;
|
||||
using StellaOps.Zastava.Webhook.Admission;
|
||||
using StellaOps.Zastava.Webhook.Authority;
|
||||
using StellaOps.Zastava.Webhook.Backend;
|
||||
using StellaOps.Zastava.Webhook.Certificates;
|
||||
using StellaOps.Zastava.Webhook.Configuration;
|
||||
using StellaOps.Zastava.Webhook.Hosting;
|
||||
using StellaOps.Zastava.Webhook.DependencyInjection;
|
||||
using StellaOps.Zastava.Webhook.Hosting;
|
||||
using StellaOps.Zastava.Webhook.Secrets;
|
||||
using StellaOps.Zastava.Webhook.Surface;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -30,11 +36,37 @@ public static class ServiceCollectionExtensions
|
||||
services.TryAddSingleton<WebhookCertificateHealthCheck>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<ZastavaRuntimeOptions>, WebhookRuntimeOptionsPostConfigure>());
|
||||
|
||||
services.AddSurfaceEnvironment(options =>
|
||||
{
|
||||
options.ComponentName = "Zastava.Webhook";
|
||||
options.AddPrefix("ZASTAVA_WEBHOOK");
|
||||
options.AddPrefix("ZASTAVA");
|
||||
options.KnownFeatureFlags.Add("admission");
|
||||
options.KnownFeatureFlags.Add("drift");
|
||||
options.TenantResolver = sp => sp.GetRequiredService<IOptions<ZastavaRuntimeOptions>>().Value.Tenant;
|
||||
});
|
||||
|
||||
services.AddSurfaceFileCache();
|
||||
services.AddSurfaceManifestStore();
|
||||
services.AddSurfaceSecrets(options =>
|
||||
{
|
||||
options.ComponentName = "Zastava.Webhook";
|
||||
options.RequiredSecretTypes.Add("attestation");
|
||||
});
|
||||
|
||||
services.TryAddSingleton(sp => sp.GetRequiredService<ISurfaceEnvironment>().Settings);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceCacheOptions>>(sp =>
|
||||
new SurfaceCacheOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceManifestStoreOptions>>(sp =>
|
||||
new SurfaceManifestStoreOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
|
||||
services.TryAddSingleton<AdmissionReviewParser>();
|
||||
services.TryAddSingleton<AdmissionResponseBuilder>();
|
||||
services.TryAddSingleton<RuntimePolicyCache>();
|
||||
services.TryAddSingleton<IImageDigestResolver, ImageDigestResolver>();
|
||||
services.TryAddSingleton<IRuntimeAdmissionPolicyService, RuntimeAdmissionPolicyService>();
|
||||
services.TryAddSingleton<IWebhookSurfaceFsClient, WebhookSurfaceFsClient>();
|
||||
services.TryAddSingleton<IWebhookSurfaceSecrets, WebhookSurfaceSecrets>();
|
||||
|
||||
services.AddHttpClient<IRuntimePolicyClient, RuntimePolicyClient>((provider, client) =>
|
||||
{
|
||||
@@ -53,8 +85,39 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
services.AddHealthChecks()
|
||||
.AddCheck<WebhookCertificateHealthCheck>("webhook_tls")
|
||||
.AddCheck<AuthorityTokenHealthCheck>("authority_token");
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
.AddCheck<AuthorityTokenHealthCheck>("authority_token");
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SurfaceCacheOptionsConfigurator : IConfigureOptions<SurfaceCacheOptions>
|
||||
{
|
||||
private readonly SurfaceEnvironmentSettings settings;
|
||||
|
||||
public SurfaceCacheOptionsConfigurator(SurfaceEnvironmentSettings settings)
|
||||
{
|
||||
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public void Configure(SurfaceCacheOptions options)
|
||||
{
|
||||
options.RootDirectory ??= settings.CacheRoot.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SurfaceManifestStoreOptionsConfigurator : IConfigureOptions<SurfaceManifestStoreOptions>
|
||||
{
|
||||
private readonly SurfaceEnvironmentSettings settings;
|
||||
|
||||
public SurfaceManifestStoreOptionsConfigurator(SurfaceEnvironmentSettings settings)
|
||||
{
|
||||
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public void Configure(SurfaceManifestStoreOptions options)
|
||||
{
|
||||
options.Bucket = string.IsNullOrWhiteSpace(settings.SurfaceFsBucket) ? options.Bucket : settings.SurfaceFsBucket;
|
||||
options.RootDirectory ??= Path.Combine(settings.CacheRoot.FullName, "manifests");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -52,7 +52,8 @@ public sealed class AdmissionResponseBuilderTests
|
||||
},
|
||||
Reasons = Array.Empty<string>(),
|
||||
FromCache = false,
|
||||
ResolutionFailed = false
|
||||
ResolutionFailed = false,
|
||||
SurfacePointer = "cas://surface-cache/manifests/tenant-a/abcd.json"
|
||||
}
|
||||
},
|
||||
BackendFailed = false,
|
||||
@@ -70,6 +71,7 @@ public sealed class AdmissionResponseBuilderTests
|
||||
Assert.NotNull(response.Response.AuditAnnotations);
|
||||
Assert.True(envelope.Decision.Images.First().HasSbomReferrers);
|
||||
Assert.StartsWith("sha256-", envelope.Decision.PodSpecDigest, StringComparison.Ordinal);
|
||||
Assert.Equal("cas://surface-cache/manifests/tenant-a/abcd.json", envelope.Decision.Images.First().Metadata["surfacePointer"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Zastava.Core.Configuration;
|
||||
using StellaOps.Zastava.Core.Diagnostics;
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
using StellaOps.Zastava.Webhook.Admission;
|
||||
using StellaOps.Zastava.Webhook.Backend;
|
||||
using StellaOps.Zastava.Webhook.Configuration;
|
||||
using StellaOps.Zastava.Webhook.Surface;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Zastava.Webhook.Tests.Admission;
|
||||
@@ -40,6 +43,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
var optionsMonitor = new StaticOptionsMonitor<ZastavaWebhookOptions>(new ZastavaWebhookOptions());
|
||||
var cache = new RuntimePolicyCache(Options.Create(optionsMonitor.CurrentValue), timeProvider, NullLogger<RuntimePolicyCache>.Instance);
|
||||
var resolver = new ImageDigestResolver();
|
||||
var surfaceSettings = CreateSurfaceSettings();
|
||||
var surfaceClient = new StubSurfaceFsClient(found: true);
|
||||
|
||||
var service = new RuntimeAdmissionPolicyService(
|
||||
policyClient,
|
||||
@@ -48,6 +53,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
optionsMonitor,
|
||||
runtimeMetrics,
|
||||
timeProvider,
|
||||
surfaceClient,
|
||||
surfaceSettings,
|
||||
NullLogger<RuntimeAdmissionPolicyService>.Instance);
|
||||
|
||||
var request = new RuntimeAdmissionRequest(
|
||||
@@ -83,6 +90,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
};
|
||||
var optionsMonitor = new StaticOptionsMonitor<ZastavaWebhookOptions>(options);
|
||||
var cache = new RuntimePolicyCache(Options.Create(optionsMonitor.CurrentValue), timeProvider, NullLogger<RuntimePolicyCache>.Instance);
|
||||
var surfaceSettings = CreateSurfaceSettings();
|
||||
var surfaceClient = new StubSurfaceFsClient(found: true);
|
||||
var service = new RuntimeAdmissionPolicyService(
|
||||
policyClient,
|
||||
new ImageDigestResolver(),
|
||||
@@ -90,6 +99,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
optionsMonitor,
|
||||
new StubRuntimeMetrics(),
|
||||
timeProvider,
|
||||
surfaceClient,
|
||||
surfaceSettings,
|
||||
NullLogger<RuntimeAdmissionPolicyService>.Instance);
|
||||
|
||||
var request = new RuntimeAdmissionRequest(
|
||||
@@ -121,6 +132,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
};
|
||||
var optionsMonitor = new StaticOptionsMonitor<ZastavaWebhookOptions>(options);
|
||||
var cache = new RuntimePolicyCache(Options.Create(optionsMonitor.CurrentValue), timeProvider, NullLogger<RuntimePolicyCache>.Instance);
|
||||
var surfaceSettings = CreateSurfaceSettings();
|
||||
var surfaceClient = new StubSurfaceFsClient(found: true);
|
||||
|
||||
var service = new RuntimeAdmissionPolicyService(
|
||||
policyClient,
|
||||
@@ -129,6 +142,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
optionsMonitor,
|
||||
new StubRuntimeMetrics(),
|
||||
timeProvider,
|
||||
surfaceClient,
|
||||
surfaceSettings,
|
||||
NullLogger<RuntimeAdmissionPolicyService>.Instance);
|
||||
|
||||
var request = new RuntimeAdmissionRequest(
|
||||
@@ -152,6 +167,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
var policyClient = new StubRuntimePolicyClient(new RuntimePolicyResponse { TtlSeconds = 300 });
|
||||
var optionsMonitor = new StaticOptionsMonitor<ZastavaWebhookOptions>(new ZastavaWebhookOptions());
|
||||
var cache = new RuntimePolicyCache(Options.Create(optionsMonitor.CurrentValue), timeProvider, NullLogger<RuntimePolicyCache>.Instance);
|
||||
var surfaceSettings = CreateSurfaceSettings();
|
||||
var surfaceClient = new StubSurfaceFsClient(found: true);
|
||||
|
||||
var service = new RuntimeAdmissionPolicyService(
|
||||
policyClient,
|
||||
@@ -160,6 +177,8 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
optionsMonitor,
|
||||
new StubRuntimeMetrics(),
|
||||
timeProvider,
|
||||
surfaceClient,
|
||||
surfaceSettings,
|
||||
NullLogger<RuntimeAdmissionPolicyService>.Instance);
|
||||
|
||||
var request = new RuntimeAdmissionRequest(
|
||||
@@ -175,6 +194,78 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
Assert.Contains("image.reference.tag_unresolved", decision.Reasons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_DeniesWhenSurfaceManifestMissing()
|
||||
{
|
||||
var timeProvider = new TestTimeProvider(new DateTimeOffset(2025, 10, 24, 12, 0, 0, TimeSpan.Zero));
|
||||
var policyClient = new StubRuntimePolicyClient(new RuntimePolicyResponse
|
||||
{
|
||||
TtlSeconds = 300,
|
||||
Results = new Dictionary<string, RuntimePolicyImageResult>
|
||||
{
|
||||
[SampleDigest] = new RuntimePolicyImageResult
|
||||
{
|
||||
PolicyVerdict = PolicyVerdict.Pass,
|
||||
Signed = true,
|
||||
HasSbom = true
|
||||
}
|
||||
}
|
||||
});
|
||||
var optionsMonitor = new StaticOptionsMonitor<ZastavaWebhookOptions>(new ZastavaWebhookOptions());
|
||||
var cache = new RuntimePolicyCache(Options.Create(optionsMonitor.CurrentValue), timeProvider, NullLogger<RuntimePolicyCache>.Instance);
|
||||
var surfaceSettings = CreateSurfaceSettings();
|
||||
var surfaceClient = new StubSurfaceFsClient(found: false);
|
||||
|
||||
var service = new RuntimeAdmissionPolicyService(
|
||||
policyClient,
|
||||
new ImageDigestResolver(),
|
||||
cache,
|
||||
optionsMonitor,
|
||||
new StubRuntimeMetrics(),
|
||||
timeProvider,
|
||||
surfaceClient,
|
||||
surfaceSettings,
|
||||
NullLogger<RuntimeAdmissionPolicyService>.Instance);
|
||||
|
||||
var request = new RuntimeAdmissionRequest(
|
||||
Namespace: "payments",
|
||||
Labels: new Dictionary<string, string>(),
|
||||
Images: new[] { $"ghcr.io/example/api@{SampleDigest}" });
|
||||
|
||||
var evaluation = await service.EvaluateAsync(request, CancellationToken.None);
|
||||
var decision = Assert.Single(evaluation.Decisions);
|
||||
Assert.False(decision.Allowed);
|
||||
Assert.Contains("surface.manifest.missing", decision.Reasons);
|
||||
}
|
||||
|
||||
private static SurfaceEnvironmentSettings CreateSurfaceSettings()
|
||||
=> new(
|
||||
new Uri("https://surface.test"),
|
||||
"surface-cache",
|
||||
null,
|
||||
new DirectoryInfo(Path.Combine(Path.GetTempPath(), "zastava-surface-tests")),
|
||||
1024,
|
||||
false,
|
||||
new[] { "admission" },
|
||||
new SurfaceSecretsConfiguration("inline", "default", null, null, null, true),
|
||||
"default",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
|
||||
private sealed class StubSurfaceFsClient : IWebhookSurfaceFsClient
|
||||
{
|
||||
private readonly bool found;
|
||||
private readonly string? pointer;
|
||||
|
||||
public StubSurfaceFsClient(bool found, string? pointer = "cas://surface-cache/manifests/default/test.json")
|
||||
{
|
||||
this.found = found;
|
||||
this.pointer = pointer;
|
||||
}
|
||||
|
||||
public Task<(bool Found, string? ManifestUri)> TryGetManifestAsync(string manifestDigest, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult((found, pointer));
|
||||
}
|
||||
|
||||
private sealed class StubRuntimePolicyClient : IRuntimePolicyClient
|
||||
{
|
||||
private readonly RuntimePolicyResponse? response;
|
||||
|
||||
Reference in New Issue
Block a user