Skip to content

Commit

Permalink
Support for Pipeline As Code Tekton (#174)
Browse files Browse the repository at this point in the history
* parse pipeline as code tekton
* untrusted checkout pipeline as code tekton
* added injection detection for script steps
  • Loading branch information
SUSTAPLE117 authored Aug 2, 2024
1 parent b7c3cae commit b8181bf
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 0 deletions.
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*\\}\\}"

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")
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

0 comments on commit b8181bf

Please sign in to comment.