diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml index 7f16f10b6d..b5c607a05c 100644 --- a/.github/workflows/analyze.yaml +++ b/.github/workflows/analyze.yaml @@ -39,6 +39,8 @@ jobs: prerelease: true outputFormat: Sarif outputPath: reports/ps-rule-results.sarif + env: + PSRULE_INPUT_FILEOBJECTS: true - name: Upload results to security tab uses: github/codeql-action/upload-sarif@v3 diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index 483ff39f8f..b7fe0f226e 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -43,6 +43,8 @@ What's changed since pre-release v3.0.0-B0198: - Bug fixes: - Fixed reason reported for `startsWith` by @BernieWhite. [#1818](https://github.com/microsoft/PSRule/issues/1818) + - Fixes CSV output of multiple lines by @BernieWhite. + [#1627](https://github.com/microsoft/PSRule/issues/1627) ## v3.0.0-B0198 (pre-release) diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index bac7c112ef..9ec61e5b85 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -322,7 +322,7 @@ task Rules { As = 'Summary' } Import-Module (Join-Path -Path $PWD -ChildPath out/modules/PSRule) -Force; - Assert-PSRule @assertParams -OutputPath reports/ps-rule-file.xml -InputPath $PWD -ErrorAction Stop; + Assert-PSRule @assertParams -OutputPath reports/ps-rule-file.xml -InputPath $PWD -ErrorAction Stop -Option @{ 'Input.FileObjects' = $True }; } task Benchmark { diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index d7ad1d4d69..953957e58e 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -553,7 +553,7 @@ "type": "boolean", "title": "File objects", "description": "Determines if file objects are processed by rules.", - "markdownDescription": "Determines if file objects are processed by rules. [See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#fileobjects)", + "markdownDescription": "Determines if file objects are processed by rules. [See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#inputfileobjects)", "default": false }, "format": { diff --git a/src/PSRule/Pipeline/Output/CsvOutputWriter.cs b/src/PSRule/Pipeline/Output/CsvOutputWriter.cs index d3ac2dfad5..c629dbc003 100644 --- a/src/PSRule/Pipeline/Output/CsvOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/CsvOutputWriter.cs @@ -25,7 +25,7 @@ internal CsvOutputWriter(PipelineWriter inner, PSRuleOption option, ShouldProces protected override string Serialize(object[] o) { WriteHeader(); - if (Option.Output.As == ResultFormat.Detail) + if (Option.Output.As.GetValueOrDefault(OutputOption.Default.As.Value) == ResultFormat.Detail) WriteDetail(o); else WriteSummary(o); @@ -127,7 +127,11 @@ private void WriteColumn(string value) return; _Builder.Append(QUOTE); - _Builder.Append(value.Replace("\"", "\"\"")); + _Builder.Append(value + .Replace("\"", "\"\"").Replace("\r\n", " ") + .Replace('\r', ' ').Replace('\n', ' ') + .Replace(" ", " ") + ); _Builder.Append(QUOTE); } @@ -136,8 +140,6 @@ private void WriteColumn(InfoString value) if (!value.HasValue) return; - _Builder.Append(QUOTE); - _Builder.Append(value.Text.Replace("\"", "\"\"")); - _Builder.Append(QUOTE); + WriteColumn(value.Text); } } diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index db1c57b0b5..8284845478 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -118,7 +118,10 @@ public void Yaml() reason: [] info: moduleName: TestModule - recommendation: Recommendation for rule 001 + recommendation: >- + Recommendation for rule 001 + + over two lines. level: Error outcome: Pass outcomeReason: Processed @@ -216,7 +219,7 @@ public void Json() ""displayName"": ""Rule 001"", ""moduleName"": ""TestModule"", ""name"": ""rule-001"", - ""recommendation"": ""Recommendation for rule 001"", + ""recommendation"": ""Recommendation for rule 001\r\nover two lines."", ""synopsis"": ""This is rule 001."" }, ""level"": ""Error"", @@ -340,6 +343,32 @@ public void NUnit3() Assert.Equal("", xml); } + [Fact] + public void Csv() + { + var option = GetOption(); + option.Repository.Url = "https://github.com/microsoft/PSRule.UnitTest"; + var output = new TestWriter(option); + var result = new InvokeResult(); + result.Add(GetPass()); + result.Add(GetFail()); + result.Add(GetFail("rid-003", SeverityLevel.Warning)); + result.Add(GetFail("rid-004", SeverityLevel.Information)); + var writer = new CsvOutputWriter(output, option, null); + writer.Begin(); + writer.WriteObject(result, false); + writer.End(); + + var actual = output.Output.OfType().FirstOrDefault(); + + Assert.Equal(@"RuleName,TargetName,TargetType,Outcome,Synopsis,Recommendation +""rule-001"",""TestObject1"",""TestType"",""Pass"",""Processed"",""This is rule 001."",""Recommendation for rule 001 over two lines."" +""rule-002"",""TestObject1"",""TestType"",""Fail"",""Processed"",""This is rule 002."",""Recommendation for rule 002"" +""rule-002"",""TestObject1"",""TestType"",""Fail"",""Processed"",""This is rule 002."",""Recommendation for rule 002"" +""rule-002"",""TestObject1"",""TestType"",""Fail"",""Processed"",""This is rule 002."",""Recommendation for rule 002"" +", actual); + } + [Fact] public void JobSummary() { @@ -367,7 +396,8 @@ public void JobSummary() private static RuleRecord GetPass() { - return new RuleRecord( + return new RuleRecord + ( runId: "run-001", ruleId: ResourceId.Parse("TestModule\\rule-001"), @ref: null, @@ -375,12 +405,13 @@ private static RuleRecord GetPass() targetName: "TestObject1", targetType: "TestType", tag: new ResourceTags(), - info: new RuleHelpInfo( + info: new RuleHelpInfo + ( "rule-001", "Rule 001", "TestModule", synopsis: new InfoString("This is rule 001."), - recommendation: new InfoString("Recommendation for rule 001") + recommendation: new InfoString("Recommendation for rule 001\r\nover two lines.") ), field: new Hashtable(), level: SeverityLevel.Error, diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 0d41007562..25e83c50ae 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -642,7 +642,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $resultCsv[1].Synopsis | Should -Be 'Test rule 3'; $resultCsv[2].RuleName | Should -Be 'WithCsv'; $resultCsv[2].Synopsis | Should -Be 'This is "a" synopsis.'; - ($resultCsv[2].Recommendation -replace "`r`n", "`n") | Should -Be "This is an extended recommendation.`n`n- That includes line breaks`n- And lists"; + ($resultCsv[2].Recommendation -replace "`r`n", "`n") | Should -Be "This is an extended recommendation. - That includes line breaks - And lists"; # Summary $result = $testObject | Invoke-PSRule @option -As Summary | Out-String; @@ -659,7 +659,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $resultCsv[1].Fail | Should -Be '1'; $resultCsv[2].RuleName | Should -Be 'WithCsv'; $resultCsv[2].Synopsis | Should -Be 'This is "a" synopsis.'; - ($resultCsv[2].Recommendation -replace "`r`n", "`n") | Should -Be "This is an extended recommendation.`n`n- That includes line breaks`n- And lists"; + ($resultCsv[2].Recommendation -replace "`r`n", "`n") | Should -Be "This is an extended recommendation. - That includes line breaks - And lists"; } It 'Sarif' {