Add PHP Analyzer Plugin and Composer Lock Data Handling
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:
StellaOps Bot
2025-11-22 14:02:49 +02:00
parent a7f3c7869a
commit b6b9ffc050
158 changed files with 16272 additions and 809 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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>