-
Notifications
You must be signed in to change notification settings - Fork 509
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: laurentsimon <[email protected]>
- Loading branch information
1 parent
beb21cc
commit 26e30e0
Showing
5 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
// Copyright 2023 OpenSSF Scorecard Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package probe | ||
|
||
import ( | ||
"bytes" | ||
"embed" | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"strings" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// RemediationEffort indicates the estimated effort necessary to remediate a finding. | ||
type RemediationEffort int | ||
|
||
const ( | ||
// RemediationEffortNone indicates a no remediation effort. | ||
RemediationEffortNone RemediationEffort = iota | ||
// RemediationEffortLow indicates a low remediation effort. | ||
RemediationEffortLow | ||
// RemediationEffortMedium indicates a medium remediation effort. | ||
RemediationEffortMedium | ||
// RemediationEffortHigh indicates a high remediation effort. | ||
RemediationEffortHigh | ||
) | ||
|
||
// Remediation represents the remediation for a finding. | ||
type Remediation struct { | ||
// Patch for machines. | ||
Patch *string `json:"patch,omitempty"` | ||
// Text for humans. | ||
Text string `json:"text"` | ||
// Text in markdown format for humans. | ||
Markdown string `json:"markdown"` | ||
// Effort to remediate. | ||
Effort RemediationEffort `json:"effort"` | ||
} | ||
|
||
// nolint: govet | ||
type jsonRemediation struct { | ||
Text []string `yaml:"text"` | ||
Markdown []string `yaml:"markdown"` | ||
Effort RemediationEffort `yaml:"effort"` | ||
} | ||
|
||
// nolint: govet | ||
type jsonProbe struct { | ||
ID string `yaml:"id"` | ||
Short string `yaml:"short"` | ||
Desc string `yaml:"desc"` | ||
Motivation string `yaml:"motivation"` | ||
Implementation string `yaml:"implementation"` | ||
Remediation jsonRemediation `yaml:"remediation"` | ||
} | ||
|
||
// nolint: govet | ||
type Probe struct { | ||
Name string | ||
Short string | ||
Motivation string | ||
Implementation string | ||
Remediation *Remediation | ||
} | ||
|
||
var errInvalid = errors.New("invalid") | ||
|
||
func fromFile(file fs.File, probeID string) (*Probe, error) { | ||
buf := new(bytes.Buffer) | ||
_, err := buf.ReadFrom(file) | ||
if err != nil { | ||
return nil, fmt.Errorf("%w: %v", errInvalid, err) | ||
} | ||
r, err := parseFromJSON(buf.Bytes()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := validate(r, probeID); err != nil { | ||
return nil, err | ||
} | ||
|
||
return &Probe{ | ||
Name: r.ID, | ||
Short: r.Short, | ||
Motivation: r.Motivation, | ||
Implementation: r.Implementation, | ||
Remediation: &Remediation{ | ||
Text: strings.Join(r.Remediation.Text, "\n"), | ||
Markdown: strings.Join(r.Remediation.Markdown, "\n"), | ||
Effort: r.Remediation.Effort, | ||
}, | ||
}, nil | ||
} | ||
|
||
// New create a new rule. | ||
func New(loc embed.FS, probeID string) (*Probe, error) { | ||
file, err := os.Open("def.yml") | ||
if err != nil { | ||
return nil, fmt.Errorf("%w", err) | ||
} | ||
defer file.Close() | ||
return fromFile(file, probeID) | ||
} | ||
|
||
func validate(r *jsonProbe, probeID string) error { | ||
if err := validateID(r.ID, probeID); err != nil { | ||
return fmt.Errorf("%w: %v", errInvalid, err) | ||
} | ||
if err := validateRemediation(r.Remediation); err != nil { | ||
return fmt.Errorf("%w: %v", errInvalid, err) | ||
} | ||
return nil | ||
} | ||
|
||
func validateID(actual, expected string) error { | ||
if actual != expected { | ||
return fmt.Errorf("%w: read '%v', expected '%v'", errInvalid, | ||
actual, expected) | ||
} | ||
return nil | ||
} | ||
|
||
func validateRemediation(r jsonRemediation) error { | ||
switch r.Effort { | ||
case RemediationEffortHigh, RemediationEffortMedium, RemediationEffortLow: | ||
return nil | ||
default: | ||
return fmt.Errorf("%w: %v", errInvalid, fmt.Sprintf("remediation '%v'", r)) | ||
} | ||
} | ||
|
||
func parseFromJSON(content []byte) (*jsonProbe, error) { | ||
r := jsonProbe{} | ||
|
||
err := yaml.Unmarshal(content, &r) | ||
if err != nil { | ||
return nil, fmt.Errorf("%w: %v", errInvalid, err) | ||
} | ||
return &r, nil | ||
} | ||
|
||
// UnmarshalYAML is a custom unmarshalling function | ||
// to transform the string into an enum. | ||
func (r *RemediationEffort) UnmarshalYAML(n *yaml.Node) error { | ||
var str string | ||
if err := n.Decode(&str); err != nil { | ||
return fmt.Errorf("%w: %v", errInvalid, err) | ||
} | ||
|
||
// nolint:goconst | ||
switch n.Value { | ||
case "Low": | ||
*r = RemediationEffortLow | ||
case "Medium": | ||
*r = RemediationEffortMedium | ||
case "High": | ||
*r = RemediationEffortHigh | ||
default: | ||
return fmt.Errorf("%w: %q", errInvalid, str) | ||
} | ||
return nil | ||
} | ||
|
||
// String stringifies the enum. | ||
func (r *RemediationEffort) String() string { | ||
switch *r { | ||
case RemediationEffortLow: | ||
return "Low" | ||
case RemediationEffortMedium: | ||
return "Medium" | ||
case RemediationEffortHigh: | ||
return "High" | ||
default: | ||
return "" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
// Copyright 2023 OpenSSF Scorecard Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package probe | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/google/go-cmp/cmp/cmpopts" | ||
) | ||
|
||
func errCmp(e1, e2 error) bool { | ||
return errors.Is(e1, e2) || errors.Is(e2, e1) | ||
} | ||
|
||
func Test_New(t *testing.T) { | ||
t.Parallel() | ||
// nolint: govet | ||
tests := []struct { | ||
name string | ||
id string | ||
path string | ||
err error | ||
probe *Probe | ||
}{ | ||
{ | ||
name: "all fields set", | ||
id: "all-fields", | ||
path: "testdata/all-fields.yml", | ||
probe: &Probe{ | ||
Name: "all-fields", | ||
Short: "short description", | ||
Implementation: "impl1 impl2\n", | ||
Motivation: "mot1 mot2\n", | ||
Remediation: &Remediation{ | ||
Text: "step1\nstep2 https://www.google.com/something", | ||
Markdown: "step1\nstep2 [google.com](https://www.google.com/something)", | ||
Effort: RemediationEffortLow, | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "mismatch probe ID", | ||
id: "mismatch-id", | ||
path: "testdata/all-fields.yml", | ||
probe: &Probe{ | ||
Name: "all-fields", | ||
Short: "short description", | ||
Implementation: "impl1 impl2\n", | ||
Motivation: "mot1 mot2\n", | ||
Remediation: &Remediation{ | ||
Text: "step1\nstep2 https://www.google.com/something", | ||
Markdown: "step1\nstep2 [google.com](https://www.google.com/something)", | ||
Effort: RemediationEffortLow, | ||
}, | ||
}, | ||
err: errInvalid, | ||
}, | ||
{ | ||
name: "invalid risk", | ||
id: "invalid-risk", | ||
path: "testdata/invalid-risk.yml", | ||
err: errInvalid, | ||
}, | ||
{ | ||
name: "invalid effort", | ||
id: "invalid-effort", | ||
path: "testdata/invalid-effort.yml", | ||
err: errInvalid, | ||
}, | ||
// TODO: mismatch id | ||
} | ||
for _, tt := range tests { | ||
tt := tt // Re-initializing variable so it is not changed while executing the closure below | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
file, err := os.Open(tt.path) | ||
if err != nil { | ||
panic(err) | ||
} | ||
defer file.Close() | ||
|
||
r, err := fromFile(file, tt.id) | ||
if err != nil || tt.err != nil { | ||
if !errCmp(err, tt.err) { | ||
t.Fatalf("unexpected error: %v", cmp.Diff(err, tt.err, cmpopts.EquateErrors())) | ||
} | ||
return | ||
} | ||
|
||
if diff := cmp.Diff(*tt.probe, *r); diff != "" { | ||
t.Errorf("mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
id: all-fields | ||
short: short description | ||
motivation: > | ||
mot1 | ||
mot2 | ||
implementation: > | ||
impl1 | ||
impl2 | ||
risk: High | ||
remediation: | ||
effort: Low | ||
text: | ||
- step1 | ||
- step2 https://www.google.com/something | ||
markdown: | ||
- step1 | ||
- step2 [google.com](https://www.google.com/something) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
short: short description | ||
desc: description | ||
motivation: > | ||
line1 | ||
line2 | ||
implementation: > | ||
line1 | ||
line2 | ||
risk: High | ||
remediation: | ||
effort: invalid | ||
text: | ||
- step1 | ||
- step2 https://www.google.com/something | ||
markdown: | ||
- step1 | ||
- step2 [google.com](https://www.google.com/something) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
short: short description | ||
desc: description | ||
motivation: > | ||
line1 | ||
line2 | ||
implementation: > | ||
line1 | ||
line2 | ||
risk: invalid | ||
remediation: | ||
effort: Low | ||
text: | ||
- step1 | ||
- step2 https://www.google.com/something | ||
markdown: | ||
- step1 | ||
- step2 [google.com](https://www.google.com/something) |