Skip to content

Commit

Permalink
feat(reporter): implement plain text issue formatter (#112)
Browse files Browse the repository at this point in the history
closes #97
  • Loading branch information
ostridm authored Dec 14, 2022
1 parent d365b2f commit 04f7ef1
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
105 changes: 105 additions & 0 deletions src/SecTester.Reporter/DefaultFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using SecTester.Scan.Models;

namespace SecTester.Reporter;

public class DefaultFormatter : Formatter
{
private const string CrLf = "\r\n";
private const char NewLine = '\n';
private const char BulletPoint = '●';
private const char Tabulation = '\t';
private const string TemplateBody = @"Issue in Bright UI: {0}
Name: {1}
Severity: {2}
Remediation:
{3}
Details:
{4}";
private const string TemplateExtraDetails = "Extra Details:\n{5}";
private const string TemplateReferences = "References:\n{6}";

public string Format(Issue issue)
{
var comments = issue.Comments ?? Array.Empty<Comment>();
var resources = issue.Resources ?? Array.Empty<string>();

var template = GenerateTemplate(comments.Any(), resources.Any());

var message = string.Format(CultureInfo.InvariantCulture,
template,
issue.Link,
issue.Name,
issue.Severity,
issue.Remedy,
issue.Details,
FormatList(comments, FormatExtraInfo),
FormatList(resources)
);

// ADHOC: adjust line endings respectively for OS environment
return message.Trim().Replace(NewLine.ToString(), Environment.NewLine);
}

private static string GenerateTemplate(bool extraInfo, bool references)
{
IEnumerable<string> templates = new List<string>
{
TemplateBody
};

if (extraInfo)
{
templates = templates.Append(TemplateExtraDetails);
}

if (references)
{
templates = templates.Append(TemplateReferences);
}

// ADHOC: `DefaultFormatter` works with LF, replace possible CRLF in verbatim strings
templates = templates.Select(x => x.Replace(CrLf, NewLine.ToString()));

return string.Join(NewLine.ToString(), templates);
}

private static string FormatExtraInfo(Comment comment)
{
var footer = comment.Links is not null && comment.Links.Any() ? CombineList(comment.Links.Prepend("Links:")) : "";
var blocks = new List<string> { comment.Text ?? "", footer }.Select(x => Indent(x));
var document = CombineList(blocks);

return CombineList(new List<string> { comment.Headline, document });
}

private static string Indent(string x, int length = 1)
{
// ADHOC: `DefaultFormatter` works with LF, replace possible CRLF for consistent parsing
var lines = x.Replace(CrLf, NewLine.ToString()).Split(NewLine);

return CombineList(
lines.Select(line => $"{new string(Tabulation, length)}{line}")
);
}

private static string FormatList<T>(IEnumerable<T> list, Func<T, string>? map = default)
where T : class
{
var formatItem = map ?? (x => x.ToString());

var items = list.Select(
x => $"{BulletPoint} {formatItem(x)}"
);

return CombineList(items);
}

private static string CombineList(IEnumerable<string> list, char sep = NewLine)
{
return string.Join(sep.ToString(), list);
}
}
2 changes: 1 addition & 1 deletion src/SecTester.Reporter/SecTester.Reporter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\SecTester.Scan\SecTester.Scan.csproj"/>
<ProjectReference Include="..\SecTester.Scan\SecTester.Scan.csproj" />
</ItemGroup>
</Project>
11 changes: 2 additions & 9 deletions src/SecTester.Runner/SecRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using SecTester.Reporter;
using SecTester.Scan;
using SecTester.Scan.Extensions;
using SecTester.Scan.Models;

namespace SecTester.Runner;

Expand Down Expand Up @@ -53,8 +52,8 @@ public static SecRunner Create(Configuration configuration)
.AddSecTesterBus()
.AddSecTesterRepeater()
.AddSecTesterScan()
.AddScoped<SecRunner>()
.AddSingleton<Formatter, DummyFormatter>();
.AddScoped<Formatter, DefaultFormatter>()
.AddScoped<SecRunner>();

var sp = collection.BuildServiceProvider();

Expand Down Expand Up @@ -108,10 +107,4 @@ public SecScan CreateScan(ScanSettingsBuilder builder)
_formatter
);
}

// TODO: remove once https://github.com/NeuraLegion/sectester-net/issues/97 has been closed
private sealed class DummyFormatter : Formatter
{
public string Format(Issue issue) => throw new NotImplementedException();
}
}
73 changes: 73 additions & 0 deletions test/SecTester.Reporter.Tests/DefaultFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace SecTester.Reporter.Tests;

public class DefaultFormatterTests
{
private const string CrLf = "\r\n";
private const char BulletPoint = '●';
private const char Tabulation = '\t';

private static readonly Issue Issue = new("pDzxcEXQC8df1fcz1QwPf9",
"Cross-site request forgery is a type of malicious website exploit.",
"Database connection crashed",
"The best way to protect against those kind of issues is making sure the Database resources are sufficient",
new Request("https://brokencrystals.com/"), new Request("https://brokencrystals.com/"),
1, Severity.Medium, Protocol.Http, DateTime.Today)
{
Link = "https://app.neuralegion.com/scans/pDzxcEXQC8df1fcz1QwPf9/issues/pDzxcEXQC8df1fcz1QwPf9"
};

private static readonly string FormattedText =
@"Issue in Bright UI: https://app.neuralegion.com/scans/pDzxcEXQC8df1fcz1QwPf9/issues/pDzxcEXQC8df1fcz1QwPf9
Name: Database connection crashed
Severity: Medium
Remediation:
The best way to protect against those kind of issues is making sure the Database resources are sufficient
Details:
Cross-site request forgery is a type of malicious website exploit.";

public static readonly IEnumerable<object[]> FormatInput = new List<object[]>
{
new object[]
{
Issue,
FormattedText
},
new object[]
{
Issue with
{
Comments = new List<Comment>
{
new("CommentHeadline", new List<string> { "https://example.com/comment/1", "https://example.com/comment/2" },
$"CommentText-1{CrLf}CommentText-2")
},
Resources = new List<string> { "https://example.com/resource/1", "https://example.com/resource/2" }
},
$@"{FormattedText}
Extra Details:
{BulletPoint} CommentHeadline
{Tabulation}CommentText-1
{Tabulation}CommentText-2
{Tabulation}Links:
{Tabulation}https://example.com/comment/1
{Tabulation}https://example.com/comment/2
References:
{BulletPoint} https://example.com/resource/1
{BulletPoint} https://example.com/resource/2"
}
};

private readonly Formatter _sut = new DefaultFormatter();

[Theory]
[MemberData(nameof(FormatInput))]
public void Format_GivenIssue_ReturnsFormattedString(Issue issue, string expected)
{
// act
var result = _sut.Format(issue);

// assert
// ADHOC: `DefaultFormatter::Format` returns CRLF/LF line endings respectively to OS environment
result.Should().Be(expected.ReplaceLineEndings(Environment.NewLine));
}
}
35 changes: 0 additions & 35 deletions test/SecTester.Reporter.Tests/FormatterTests.cs

This file was deleted.

2 changes: 1 addition & 1 deletion test/SecTester.Reporter.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
global using FluentAssertions;
global using NSubstitute;
global using SecTester.Scan.Models;
global using Xunit;
global using Microsoft.Extensions.DependencyInjection;

0 comments on commit 04f7ef1

Please sign in to comment.