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

Support for Pipeline As Code Tekton #174

Merged
merged 5 commits into from
Aug 2, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions models/package_insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type PackageInsights struct {
GithubActionsMetadata []GithubActionsMetadata `json:"github_actions_metadata"`
GitlabciConfigs []GitlabciConfig `json:"gitlabci_configs"`
AzurePipelines []AzurePipeline `json:"azure_pipelines"`
PipelineAsCodeTekton []PipelineAsCodeTekton `json:"pipeline_as_code_tekton"`
}

func (p *PackageInsights) GetSourceGitRepoURI() string {
Expand Down
61 changes: 61 additions & 0 deletions models/pipeline_as_code_tekton.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package models

import "gopkg.in/yaml.v3"

type PipelineAsCodeTekton struct {
ApiVersion string `json:"api_version" yaml:"apiVersion"`
Kind string `json:"kind"`
Metadata struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata"`
Spec PipelineRunSpec `json:"spec,omitempty" yaml:"spec"`

Path string `json:"path" yaml:"-"`
}

type PipelineRunSpec struct {
PipelineSpec *PipelineSpec `json:"pipeline_spec,omitempty" yaml:"pipelineSpec"`
}

type PipelineSpec struct {
Tasks []PipelineTask `json:"tasks,omitempty" yaml:"tasks"`
}

type PipelineTask struct {
Name string `json:"name,omitempty"`

TaskSpec *TaskSpec `json:"task_spec,omitempty" yaml:"taskSpec"`
}

type TaskSpec struct {
Steps []Step `json:"steps,omitempty"`
}

type Step struct {
Name string `json:"name"`
Script string `json:"script,omitempty"`
Lines map[string]int `json:"lines" yaml:"-"`
}

func (o *Step) UnmarshalYAML(node *yaml.Node) error {
type step Step
var s step
if err := node.Decode(&s); err != nil {
return err
}

if node.Kind == yaml.MappingNode {
s.Lines = map[string]int{"start": node.Line}
for i := 0; i < len(node.Content); i += 2 {
key := node.Content[i].Value
switch key {
case "script":
s.Lines[key] = node.Content[i+1].Line
}
}
}

*o = Step(s)
return nil
}
28 changes: 28 additions & 0 deletions opa/rego/rules/injection.rego
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,31 @@ results contains poutine.finding(rule, pkg.purl, {
exprs := azure_injections(step[attr])
count(exprs) > 0
}

patterns.pipeline_as_code_tekton contains "\\{\\{\\s*(body\\.pull_request\\.(title|user\\.email|body)|source_branch)\\s*\\}\\}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed an issue #184 to add more


pipeline_as_code_tekton_injections(str) = {expr |
match := regex.find_n(patterns.pipeline_as_code_tekton[_], str, -1)[_]
expr := regex.find_all_string_submatch_n("\\{\\{\\s*([^}]+?)\\s*\\}\\}", match, 1)[0][1]
}

results contains poutine.finding(rule, pkg.purl, {
"path": pipeline.path,
"job": task.name,
"step": step_idx,
"line": step.lines["start"],
"details": sprintf("Sources: %s", [concat(" ", exprs)]),
}) if {
pkg := input.packages[_]
pipeline := pkg.pipeline_as_code_tekton[_]
contains(pipeline.api_version, "tekton.dev")
pipeline.kind == "PipelineRun"
contains(pipeline.metadata.annotations["pipelinesascode.tekton.dev/on-event"], "pull_request")
task := pipeline.spec.pipeline_spec.tasks[_]
step := task.task_spec.steps[step_idx]

exprs := pipeline_as_code_tekton_injections(step.script)
count(exprs) > 0
}


24 changes: 24 additions & 0 deletions opa/rego/rules/untrusted_checkout_exec.rego
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ build_commands[cmd] = {
"bundler": {"bundle install", "bundle exec "},
"ant": {"^ant "},
"mkdocs": {"mkdocs build"},
"vale": {"vale "},
}[cmd]

results contains poutine.finding(rule, pkg_purl, {
Expand Down Expand Up @@ -134,3 +135,26 @@ find_ado_checkout(stage) := xs if {
s[step_attr] == "self"
}
}

# Pipeline As Code Tekton

results contains poutine.finding(rule, pkg.purl, {
"path": pipeline.path,
"job": task.name,
"step": step_idx,
"line": step.lines["script"],
"details": sprintf("Detected usage of `%s`", [cmd]),
}) if {
pkg := input.packages[_]
pipeline := pkg.pipeline_as_code_tekton[_]
contains(pipeline.api_version, "tekton.dev")
pipeline.kind == "PipelineRun"
contains(pipeline.metadata.annotations["pipelinesascode.tekton.dev/on-event"], "pull_request")
contains(pipeline.metadata.annotations["pipelinesascode.tekton.dev/task"], "git-clone")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we can ship with just that, I'll file an issue #185

task := pipeline.spec.pipeline_spec.tasks[_]
step := task.task_spec.steps[step_idx]
regex.match(
sprintf("([^a-z]|^)(%v)", [concat("|", build_commands[cmd])]),
step.script,
)
}
22 changes: 22 additions & 0 deletions scanner/inventory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,28 @@ func TestFindings(t *testing.T) {
Details: "Detected usage of `npm`",
},
},
{
RuleId: "untrusted_checkout_exec",
Purl: purl,
Meta: opa.FindingMeta{
Path: ".tekton/pipeline-as-code-tekton.yml",
Line: 43,
Job: "vale",
Step: "0",
Details: "Detected usage of `vale`",
},
},
{
RuleId: "injection",
Purl: purl,
Meta: opa.FindingMeta{
Path: ".tekton/pipeline-as-code-tekton.yml",
Line: 45,
Job: "vale",
Step: "1",
Details: "Sources: body.pull_request.body",
},
},
}

assert.Equal(t, len(findings), len(results.Findings))
Expand Down
25 changes: 25 additions & 0 deletions scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,30 @@ func parseGitlabCi(scanner *Scanner, filePath string, fileInfo fs.FileInfo) erro
return nil
}

func parsePipelineAsCodeTekton(scanner *Scanner, filePath string, fileInfo fs.FileInfo) error {
relPath, err := filepath.Rel(scanner.Path, filePath)
if err != nil {
return err
}

data, err := os.ReadFile(filePath)
if err != nil {
return err
}

pipelineAsCode := models.PipelineAsCodeTekton{}
err = yaml.Unmarshal(data, &pipelineAsCode)
if err != nil {
log.Debug().Err(err).Str("file", relPath).Msg("failed to unmarshal pipeline as code yaml file")
return nil
}

pipelineAsCode.Path = relPath
scanner.Package.PipelineAsCodeTekton = append(scanner.Package.PipelineAsCodeTekton, pipelineAsCode)

return nil
}

type Scanner struct {
Path string
Package *models.PackageInsights
Expand All @@ -169,6 +193,7 @@ func NewScanner(path string) Scanner {
ParseFuncs: map[*regexp.Regexp]parseFunc{
regexp.MustCompile(`(\b|/)action\.ya?ml$`): parseGithubActionsMetadata,
regexp.MustCompile(`^\.github/workflows/[^/]+\.ya?ml$`): parseGithubWorkflows,
regexp.MustCompile(`^\.tekton/[^/]+\.ya?ml$`): parsePipelineAsCodeTekton,
regexp.MustCompile(`\.?azure-pipelines(-.+)?\.ya?ml$`): parseAzurePipelines,
regexp.MustCompile(`\.?gitlab-ci(-.+)?\.ya?ml$`): parseGitlabCi,
},
Expand Down
51 changes: 51 additions & 0 deletions scanner/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package scanner

import (
"context"
"github.com/boostsecurityio/poutine/models"
"github.com/boostsecurityio/poutine/opa"
"github.com/stretchr/testify/assert"
"testing"
Expand Down Expand Up @@ -69,3 +70,53 @@ func TestRun(t *testing.T) {
assert.Contains(t, s.Package.PackageDependencies, "pkg:docker/alpine%3Alatest")
assert.Equal(t, 3, len(s.Package.GitlabciConfigs))
}

func TestPipelineAsCodeTekton(t *testing.T) {
s := NewScanner("testdata")
o, _ := opa.NewOpa()
err := s.Run(context.TODO(), o)
assert.NoError(t, err)

pipelines := s.Package.PipelineAsCodeTekton

assert.Len(t, pipelines, 1)
expectedAnnotations := map[string]string{
"pipelinesascode.tekton.dev/on-event": "[push, pull_request]",
"pipelinesascode.tekton.dev/on-target-branch": "[*]",
"pipelinesascode.tekton.dev/task": "[git-clone]",
}
expectedPipeline := models.PipelineAsCodeTekton{
ApiVersion: "tekton.dev/v1beta1",
Kind: "PipelineRun",
Metadata: struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
}{
Name: "linters",
Annotations: expectedAnnotations,
},
Spec: models.PipelineRunSpec{
PipelineSpec: &models.PipelineSpec{
Tasks: []models.PipelineTask{
{
Name: "fetchit",
},
{
Name: "vale",
TaskSpec: &models.TaskSpec{
Steps: []models.Step{
{
Name: "vale-lint",
Script: "vale docs/content --minAlertLevel=error --output=line\n",
Lines: map[string]int{"script": 43, "start": 40},
},
},
},
},
},
},
},
}
assert.Equal(t, expectedPipeline.Metadata, pipelines[0].Metadata)
assert.Equal(t, expectedPipeline.Spec.PipelineSpec.Tasks[1].TaskSpec.Steps[0], pipelines[0].Spec.PipelineSpec.Tasks[1].TaskSpec.Steps[0])
}
63 changes: 63 additions & 0 deletions scanner/testdata/.tekton/pipeline-as-code-tekton.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: linters
annotations:
pipelinesascode.tekton.dev/on-event: "[push, pull_request]"
pipelinesascode.tekton.dev/on-target-branch: "[*]"
pipelinesascode.tekton.dev/task: "[git-clone]"
spec:
params:
- name: repo_url
value: "{{repo_url}}"
- name: revision
value: "{{revision}}"
pipelineSpec:
params:
- name: repo_url
- name: revision
tasks:
- name: fetchit
displayName: "Fetch git repository"
params:
- name: url
value: $(params.repo_url)
- name: revision
value: $(params.revision)
taskRef:
name: git-clone
workspaces:
- name: output
workspace: source
- name: vale
displayName: "Spelling and Grammar"
runAfter:
- fetchit
taskSpec:
workspaces:
- name: source
steps:
- name: vale-lint
image: jdkato/vale
workingDir: $(workspaces.source.path)
script: |
vale docs/content --minAlertLevel=error --output=line
- name: injection
image: jdkato/vale
workingDir: $(workspaces.source.path)
script: |
binary {{body.pull_request.body}}
workspaces:
- name: source
workspace: source
workspaces:
- name: source
workspaces:
- name: source
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi