Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic initial implementation of oss-fresh-tool. #410

Merged
merged 1 commit into from Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/OSSGadget.sln
Expand Up @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared.Lib", "Shared\Shared
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared.CLI", "Shared.CLI\Shared.CLI.csproj", "{66CE54D2-40AA-41CB-A487-3FE44E38BFE0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "oss-fresh", "oss-fresh\oss-fresh.csproj", "{5E0E66AB-D141-42A1-B53F-BEE63FBD62FB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -115,6 +117,10 @@ Global
{66CE54D2-40AA-41CB-A487-3FE44E38BFE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66CE54D2-40AA-41CB-A487-3FE44E38BFE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66CE54D2-40AA-41CB-A487-3FE44E38BFE0}.Release|Any CPU.Build.0 = Release|Any CPU
{5E0E66AB-D141-42A1-B53F-BEE63FBD62FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5E0E66AB-D141-42A1-B53F-BEE63FBD62FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E0E66AB-D141-42A1-B53F-BEE63FBD62FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5E0E66AB-D141-42A1-B53F-BEE63FBD62FB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
212 changes: 212 additions & 0 deletions src/oss-fresh/FreshTool.cs
@@ -0,0 +1,212 @@
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.

using CommandLine;
using CommandLine.Text;
using Microsoft.CodeAnalysis.Sarif;
using Microsoft.CST.OpenSource.Shared;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SemanticVersioning;
using static Microsoft.CST.OpenSource.Shared.OutputBuilderFactory;

namespace Microsoft.CST.OpenSource
{
using AngleSharp;
using PackageManagers;
using PackageUrl;
using System.Text.Json;
using System.Text.RegularExpressions;

public class FreshTool : OSSGadget
{
public FreshTool(ProjectManagerFactory projectManagerFactory) : base(projectManagerFactory)
{
}

public FreshTool() : this(new ProjectManagerFactory())
{
}

public class Options
{
[Usage()]
public static IEnumerable<Example> Examples
{
get
{
return new List<Example>() {
new Example("Find the source code repository for the given package", new Options { Targets = new List<string>() {"[options]", "package-url..." } })};
}
}

[Option('f', "format", Required = false, Default = "text",
HelpText = "specify the output format(text|sarifv1|sarifv2)")]
public string Format { get; set; } = "text";

[Option('o', "output-file", Required = false, Default = "",
HelpText = "send the command output to a file instead of stdout")]
public string OutputFile { get; set; } = "";

[Option('m', "max-age-maintained", Required = false, Default = 30 * 18,
HelpText = "maximum age of versions for still-maintained projects, 0 to disable")]
public int MaxAgeMaintained { get; set; }

[Option('u', "max-age-unmaintained", Required = false, Default = 30 * 48,
HelpText = "maximum age of versions for unmaintained projects, 0 to disable")]
public int MaxAgeUnmaintained { get; set; }

[Option('v', "max-out-of-date-versions", Required = false, Default = 6,
HelpText = "maximum number of versions out of date, 0 to disable")]
public int MaxOutOfDateVersions { get; set; }

[Option('r', "filter", Required = false, Default = null,
HelpText = "filter versions by regular expression")]
public string? Filter { get; set; }

[Value(0, Required = true,
HelpText = "PackgeURL(s) specifier to analyze (required, repeats OK)", Hidden = true)] // capture all targets to analyze
public IEnumerable<string>? Targets { get; set; }
}

static async Task Main(string[] args)
{
FreshTool freshTool = new FreshTool();
await freshTool.ParseOptions<Options>(args).WithParsedAsync(freshTool.RunAsync);
}

private async Task RunAsync(Options options)
{
// select output destination and format
SelectOutput(options.OutputFile);
IOutputBuilder outputBuilder = SelectFormat(options.Format);

int maintainedThresholdDays = options.MaxAgeMaintained > 0 ? options.MaxAgeMaintained : int.MaxValue;
int nonMaintainedThresholdDays = options.MaxAgeUnmaintained > 0 ? options.MaxAgeUnmaintained : int.MaxValue;
int maintainedThresholdVersions = options.MaxOutOfDateVersions;

string? versionFilter = options.Filter;

int safeDays = 90;
DateTime NOW = DateTime.Now;

maintainedThresholdDays = Math.Max(maintainedThresholdDays, safeDays);
nonMaintainedThresholdDays = Math.Max(nonMaintainedThresholdDays, safeDays);

if (options.Targets is IList<string> targetList && targetList.Count > 0)
{
foreach (string? target in targetList)
{
try
{
PackageURL? purl = new PackageURL(target);
BaseMetadataSource metadataSource = new LibrariesIoMetadataSource();
Logger.Info("Collecting metadata for {0}", purl);
JsonDocument? metadata = await metadataSource.GetMetadataForPackageUrlAsync(purl, true);
if (metadata != null)
{
JsonElement root = metadata.RootElement;

string? latestRelease = root.GetProperty("latest_release_number").GetString();
DateTime latestReleasePublishedAt = root.GetProperty("latest_release_published_at").GetDateTime();
bool stillMaintained = (NOW - latestReleasePublishedAt).TotalDays < maintainedThresholdDays;

// Extract versions
IEnumerable<JsonElement> versions = root.GetProperty("versions").EnumerateArray();

// Filter if needed
if (versionFilter != null)
{
Regex versionFilterRegex = new Regex(versionFilter, RegexOptions.Compiled);
versions = versions.Where(elt => {
string? _version = elt.GetProperty("number").GetString();
if (_version != null)
{
return versionFilterRegex.IsMatch(_version);
}
return true;
});
}
// Order by semantic version
versions = versions.OrderBy(elt => {
try
{
string? _v = elt.GetProperty("number").GetString();
if (_v == null)
{
_v = "0.0.0";
} else if (_v.Count(ch => ch == '.') == 1)
{
_v = _v + ".0";
}
return new SemanticVersioning.Version(_v, true);
}
catch(Exception)
{
return new SemanticVersioning.Version("0.0.0");
}
});

int versionIndex = 0;
foreach (JsonElement version in versions)
{
++versionIndex;
string? versionName = version.GetProperty("number").GetString();
DateTime publishedAt = version.GetProperty("published_at").GetDateTime();
string? resultMessage = null;

if (stillMaintained)
{
if ((NOW - publishedAt).TotalDays > maintainedThresholdDays)
{
resultMessage = $"This version {versionName} was published more than {maintainedThresholdDays} days ago.";
}

if (maintainedThresholdVersions > 0 &&
versionIndex < (versions.Count() - maintainedThresholdVersions))
{
if ((NOW - publishedAt).TotalDays > safeDays)
{
if (resultMessage != null )
{
resultMessage += $" In addition, this version was more than {maintainedThresholdVersions} versions out of date.";
}
else
{
resultMessage = $"This version {versionName} was more than {maintainedThresholdVersions} versions out of date.";
}
}
}
}
else
{
if ((NOW - publishedAt).TotalDays > nonMaintainedThresholdDays)
{
resultMessage = $"This version {versionName} was published more than {nonMaintainedThresholdDays} days ago.";
}
}

// Write output
if (resultMessage != null)
{
Console.WriteLine(resultMessage);
}
else
{
Console.WriteLine($"This version {versionName} is current.");
}

}
}
}
catch (Exception ex)
{
Logger.Warn("Error processing {0}: {1}", target, ex.Message);
}
}
}
}
}
}
35 changes: 35 additions & 0 deletions src/oss-fresh/oss-fresh.csproj
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">


<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Microsoft.CST.OpenSource</RootNamespace>
<Description>OSS Gadget - Package Freshness Calculator</Description>
<RepositoryType>GitHub</RepositoryType>
<RepositoryUrl>https://github.com/Microsoft/OSSGadget</RepositoryUrl>
<StartupObject>Microsoft.CST.OpenSource.FreshTool</StartupObject>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<copyright>© Microsoft Corporation. All rights reserved.</copyright>
<Company>Microsoft</Company>
<Authors>Microsoft</Authors>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
<PackAsTool>true</PackAsTool>
<PackageId>Microsoft.CST.OSSGadget.FreshTool.CLI</PackageId>
<PackageVersion>0.0.0</PackageVersion>
<PackageProjectUrl>https://github.com/Microsoft/OSSGadget</PackageProjectUrl>
<PackageTags>Security Scanner</PackageTags>
<ToolCommandName>oss-fresh</ToolCommandName>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageIcon>icon-128.png</PackageIcon>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Shared.CLI\Shared.CLI.csproj" />
</ItemGroup>

</Project>