Skip to content

Commit

Permalink
Basic initial implementation of oss-fresh-tool. (#410)
Browse files Browse the repository at this point in the history
  • Loading branch information
scovetta authored Mar 21, 2023
1 parent d064406 commit aaa3c3d
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/OSSGadget.sln
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit aaa3c3d

Please sign in to comment.