up
This commit is contained in:
@@ -33,6 +33,7 @@ internal static class CommandFactory
|
||||
root.Add(BuildScannerCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildScanCommand(services, options, verboseOption, cancellationToken));
|
||||
root.Add(BuildRubyCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildPhpCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSourcesCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildAocCommand(services, verboseOption, cancellationToken));
|
||||
@@ -252,6 +253,40 @@ internal static class CommandFactory
|
||||
return ruby;
|
||||
}
|
||||
|
||||
private static Command BuildPhpCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var php = new Command("php", "Work with PHP analyzer outputs.");
|
||||
|
||||
var inspect = new Command("inspect", "Inspect a local PHP workspace.");
|
||||
var inspectRootOption = new Option<string?>("--root")
|
||||
{
|
||||
Description = "Path to the PHP workspace (defaults to current directory)."
|
||||
};
|
||||
var inspectFormatOption = new Option<string?>("--format")
|
||||
{
|
||||
Description = "Output format (table or json)."
|
||||
};
|
||||
|
||||
inspect.Add(inspectRootOption);
|
||||
inspect.Add(inspectFormatOption);
|
||||
inspect.SetAction((parseResult, _) =>
|
||||
{
|
||||
var root = parseResult.GetValue(inspectRootOption);
|
||||
var format = parseResult.GetValue(inspectFormatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandlePhpInspectAsync(
|
||||
services,
|
||||
root,
|
||||
format,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
php.Add(inspect);
|
||||
return php;
|
||||
}
|
||||
|
||||
private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var kms = new Command("kms", "Manage file-backed signing keys.");
|
||||
|
||||
@@ -38,6 +38,7 @@ using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
@@ -7154,6 +7155,122 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandlePhpInspectAsync(
|
||||
IServiceProvider services,
|
||||
string? rootPath,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("php-inspect");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
var previousLevel = verbosity.MinimumLevel;
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
using var activity = CliActivitySource.Instance.StartActivity("cli.php.inspect", ActivityKind.Internal);
|
||||
activity?.SetTag("stellaops.cli.command", "php inspect");
|
||||
using var duration = CliMetrics.MeasureCommandDuration("php inspect");
|
||||
|
||||
var outcome = "unknown";
|
||||
try
|
||||
{
|
||||
var normalizedFormat = string.IsNullOrWhiteSpace(format)
|
||||
? "table"
|
||||
: format.Trim().ToLowerInvariant();
|
||||
if (normalizedFormat is not ("table" or "json"))
|
||||
{
|
||||
throw new InvalidOperationException("Format must be either 'table' or 'json'.");
|
||||
}
|
||||
|
||||
var targetRoot = string.IsNullOrWhiteSpace(rootPath)
|
||||
? Directory.GetCurrentDirectory()
|
||||
: Path.GetFullPath(rootPath);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Directory '{targetRoot}' was not found.");
|
||||
}
|
||||
|
||||
logger.LogInformation("Inspecting PHP workspace in {Root}.", targetRoot);
|
||||
activity?.SetTag("stellaops.cli.php.root", targetRoot);
|
||||
|
||||
var engine = new LanguageAnalyzerEngine(new ILanguageAnalyzer[] { new PhpLanguageAnalyzer() });
|
||||
var context = new LanguageAnalyzerContext(targetRoot, TimeProvider.System);
|
||||
var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
var report = PhpInspectReport.Create(result.ToSnapshots());
|
||||
|
||||
activity?.SetTag("stellaops.cli.php.package_count", report.Packages.Count);
|
||||
|
||||
if (string.Equals(normalizedFormat, "json", StringComparison.Ordinal))
|
||||
{
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(report, options));
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderPhpInspectReport(report);
|
||||
}
|
||||
|
||||
outcome = report.Packages.Count == 0 ? "empty" : "ok";
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
outcome = "not_found";
|
||||
logger.LogError(ex.Message);
|
||||
Environment.ExitCode = 71;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
outcome = "invalid";
|
||||
logger.LogError(ex.Message);
|
||||
Environment.ExitCode = 64;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
outcome = "error";
|
||||
logger.LogError(ex, "PHP inspect failed.");
|
||||
Environment.ExitCode = 70;
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
CliMetrics.RecordPhpInspect(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderPhpInspectReport(PhpInspectReport report)
|
||||
{
|
||||
if (!report.Packages.Any())
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]No PHP packages detected.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table().Border(TableBorder.Rounded);
|
||||
table.AddColumn("Package");
|
||||
table.AddColumn("Version");
|
||||
table.AddColumn("Type");
|
||||
table.AddColumn(new TableColumn("Lockfile").NoWrap());
|
||||
table.AddColumn("Dev");
|
||||
|
||||
foreach (var entry in report.Packages)
|
||||
{
|
||||
var dev = entry.IsDev ? "[grey]yes[/]" : "-";
|
||||
table.AddRow(
|
||||
Markup.Escape(entry.Name),
|
||||
Markup.Escape(entry.Version ?? "-"),
|
||||
Markup.Escape(entry.Type ?? "-"),
|
||||
Markup.Escape(entry.Lockfile ?? "-"),
|
||||
dev);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
|
||||
private static void RenderRubyInspectReport(RubyInspectReport report)
|
||||
{
|
||||
if (!report.Packages.Any())
|
||||
@@ -7662,6 +7779,113 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PhpInspectReport
|
||||
{
|
||||
[JsonPropertyName("packages")]
|
||||
public IReadOnlyList<PhpInspectEntry> Packages { get; }
|
||||
|
||||
private PhpInspectReport(IReadOnlyList<PhpInspectEntry> packages)
|
||||
{
|
||||
Packages = packages;
|
||||
}
|
||||
|
||||
public static PhpInspectReport Create(IEnumerable<LanguageComponentSnapshot>? snapshots)
|
||||
{
|
||||
var source = snapshots?.ToArray() ?? Array.Empty<LanguageComponentSnapshot>();
|
||||
|
||||
var entries = source
|
||||
.Where(static snapshot => string.Equals(snapshot.Type, "composer", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(PhpInspectEntry.FromSnapshot)
|
||||
.OrderBy(static entry => entry.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(static entry => entry.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
return new PhpInspectReport(entries);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PhpInspectEntry(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("type")] string? Type,
|
||||
[property: JsonPropertyName("lockfile")] string? Lockfile,
|
||||
[property: JsonPropertyName("isDev")] bool IsDev,
|
||||
[property: JsonPropertyName("source")] string? Source,
|
||||
[property: JsonPropertyName("distSha")] string? DistSha)
|
||||
{
|
||||
public static PhpInspectEntry FromSnapshot(LanguageComponentSnapshot snapshot)
|
||||
{
|
||||
var metadata = PhpMetadataHelpers.Clone(snapshot.Metadata);
|
||||
var type = PhpMetadataHelpers.GetString(metadata, "type");
|
||||
var lockfile = PhpMetadataHelpers.GetString(metadata, "lockfile");
|
||||
var isDev = PhpMetadataHelpers.GetBool(metadata, "isDev") ?? false;
|
||||
var source = PhpMetadataHelpers.GetString(metadata, "source");
|
||||
var distSha = PhpMetadataHelpers.GetString(metadata, "distSha");
|
||||
|
||||
return new PhpInspectEntry(
|
||||
snapshot.Name,
|
||||
snapshot.Version,
|
||||
type,
|
||||
lockfile,
|
||||
isDev,
|
||||
source,
|
||||
distSha);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PhpMetadataHelpers
|
||||
{
|
||||
public static IDictionary<string, string?> Clone(IDictionary<string, string?>? metadata)
|
||||
{
|
||||
if (metadata is null || metadata.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var clone = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pair in metadata)
|
||||
{
|
||||
clone[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public static string? GetString(IDictionary<string, string?> metadata, string key)
|
||||
{
|
||||
if (metadata.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
foreach (var pair in metadata)
|
||||
{
|
||||
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool? GetBool(IDictionary<string, string?> metadata, string key)
|
||||
{
|
||||
var value = GetString(metadata, key);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bool.TryParse(value, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record LockValidationEntry(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
|
||||
Reference in New Issue
Block a user