Skip to content
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
63 changes: 63 additions & 0 deletions semgrep-scan/rules/gha-extras.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# GitHub Actions extras — workflow-misuse patterns that aren't in the
# OWASP CICD canon but are real attack surface for SC's threat model
# (parses customer secrets, runs in customer CI with elevated perms).

rules:
- id: gha-workflow-level-secret-env
message: >-
Workflow-level `env:` block defines a variable with `secrets.*`.
That value leaks to EVERY step in EVERY job — including any
third-party action call inside the workflow. Scope the secret
to the specific job or step that needs it.
severity: ERROR
languages: [yaml]
paths:
include: ["**/.github/workflows/*.yml", "**/.github/workflows/*.yaml"]
pattern-regex: '(?ms)\Aname:[^\n]*\n[\s\S]*?\non:[^\n]*\n[\s\S]*?\nenv:\s*\n(?:[ \t]+[^\n]+\n)*?[ \t]+[A-Z_][A-Z0-9_]*:\s*\$\{\{\s*secrets\.'

- id: gha-upload-artifact-secrets-leak
message: >-
`actions/upload-artifact` path matches a credential / secret file
pattern (`.env`, `*.pem`, `*.key`, `secrets/**`). Artifacts are
retained for ≤ 90 days and downloadable by anyone with repo read
access — never publish credentials this way.
severity: ERROR
languages: [generic]
paths:
include: ["**/.github/workflows/*.yml", "**/.github/workflows/*.yaml"]
# Single multi-line regex spanning `uses: actions/upload-artifact@` to
# a `path:` line within ~500 chars (covers the typical `with:` block).
pattern-regex: '(?s)uses:\s*actions/upload-artifact@[\s\S]{1,500}?path:\s*["'']?[^\n]*(?:\.(?:env|pem|key)|secrets/)'

- id: gha-pull-request-target-implicit-base-ref
message: >-
`pull_request_target` defaults `actions/checkout` to the base
branch — but any step run after `checkout` with `ref:` set to a
`github.event.pull_request.head.*` value runs FORK code with the
target's elevated permissions (the canonical PPE vector). Pin
checkout explicitly to a trusted ref or remove `pull_request_target`.
severity: ERROR
languages: [generic]
paths:
include: ["**/.github/workflows/*.yml", "**/.github/workflows/*.yaml"]
# File-level: fires if file has `pull_request_target` trigger AND
# `actions/checkout`, unless `github.event.workflow_run` (the
# `workflow_run`-fed safe-ref pattern) is also present somewhere.
patterns:
- pattern-regex: '(?ms)(?:\A|\n)on:\s*\n\s*pull_request_target\b[\s\S]{1,30000}?uses:\s*actions/checkout@'
- pattern-not-regex: 'github\.event\.workflow_run'

- id: gha-github-token-in-github-env
message: >-
Writing `GITHUB_TOKEN` (or a derived alias) to `$GITHUB_ENV`
persists the token in the runner's environment for the remainder
of the job — every subsequent step, including third-party actions
run via `uses:`, can read it. Pass tokens via per-step `env:`
mappings instead.
severity: ERROR
languages: [generic]
paths:
include: ["**/.github/workflows/*.yml", "**/.github/workflows/*.yaml"]
pattern-either:
- pattern-regex: 'echo\s+["'']?[A-Z_]*TOKEN[A-Z_]*=\$\{?\{?\s*secrets\.GITHUB_TOKEN'
- pattern-regex: 'echo\s+["'']?[A-Z_]*TOKEN[A-Z_]*=[^\n]+>>\s*\$GITHUB_ENV'
178 changes: 178 additions & 0 deletions semgrep-scan/rules/go-canon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Go security canon — gosec G-series + dgryski/semgrep-go patterns
# that CodeQL's `security-extended` Go pack misses. Focused on the
# crypto + parsing surfaces in SC (secrets cache, attestation predicate
# parsing, OIDC JWT validation, downloaded-binary hardening).

rules:
- id: go-hmac-not-constant-time
message: >-
HMAC / MAC comparison uses `bytes.Equal` or a non-constant-time
operator. The byte-comparison primitive leaks timing information
that lets an attacker reconstruct the expected MAC byte-by-byte.
Use `hmac.Equal` (or `crypto/subtle.ConstantTimeCompare`).
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
exclude: ["**/*_test.go"]
patterns:
- pattern-either:
- pattern: bytes.Equal($A, $B)
- metavariable-regex:
metavariable: $A
regex: '(mac|hmac|HMAC|MAC|signature|sig|tag)'

- id: go-cipher-deterministic-nonce
message: >-
AEAD cipher seal/open is invoked with a zero / constant nonce. The
whole point of an AEAD is per-message nonce uniqueness; reusing a
nonce under the same key catastrophically breaks confidentiality
and integrity (especially for GCM / ChaCha20-Poly1305). Use
`crypto/rand.Read(nonce[:])` per message.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
exclude: ["**/*_test.go"]
pattern-either:
- pattern: |
$NONCE := make([]byte, $AEAD.NonceSize())
...
$AEAD.Seal($DST, $NONCE, $PLAINTEXT, $AAD)
- pattern: |
$NONCE := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
...
$AEAD.Seal(...)

# Note: a `go-yaml-unmarshal-into-interface` rule was drafted and ran in
# round 5 (21 findings) but was DROPPED in round 6 after triage: SC's
# untyped-unmarshal sites are intentional (generic YAML inspection for
# diff/obfuscation/AI-analysis tooling, where the result isn't used for
# security decisions). Taint-aware coverage for the cases that DO matter
# (untrusted-source flow → unmarshal) lives in the `p/golang` registry
# pack opted into via .github/workflows/semgrep.yml.

- id: go-jwt-parse-unverified
message: >-
`jwt.ParseUnverified` skips signature validation entirely. Even
"just reading claims" requires verification because the claims
themselves are attacker-controlled until the signature is checked.
Use `jwt.Parse(..., keyfunc)` + `WithValidMethods` + audience and
issuer checks.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
exclude: ["**/*_test.go"]
pattern-either:
- pattern: jwt.ParseUnverified(...)
- pattern: $JWT.ParseUnverified(...)

- id: go-jwt-none-algorithm
message: >-
JWT verification accepts the `none` algorithm. A `none`-algorithm
JWT has no signature; any caller can claim any identity. Restrict
with `WithValidMethods([]string{"RS256", "ES256", ...})` or the
v5+ `WithValidAlg` option.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
exclude: ["**/*_test.go"]
pattern-either:
- pattern-regex: 'jwt\.SigningMethod(?:HS|RS|ES|PS)?None'
- pattern-regex: '["'']alg["'']\s*:\s*["'']none["'']'

- id: go-http-client-no-timeout
message: >-
`&http.Client{}` literal does not set `Timeout`. Default is "no
timeout" — a malicious or slow upstream hangs the request
indefinitely, exhausting goroutines/file descriptors (slowloris
class DoS). Set an explicit `Timeout: 30 * time.Second` (or a
reasonable bound for the caller's use case).
severity: WARNING
languages: [go]
paths:
include: ["**/*.go"]
exclude: ["**/*_test.go"]
patterns:
- pattern: '&http.Client{...}'
- pattern-not: '&http.Client{..., Timeout: ..., ...}'

- id: go-tls-min-version-below-1-2
message: >-
`tls.Config{MinVersion: ...}` below `tls.VersionTLS12` allows
vulnerable TLS 1.0 / 1.1 handshakes. Modern stacks should pin
`MinVersion: tls.VersionTLS13` (or 1.2 as the floor).
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
exclude: ["**/*_test.go"]
pattern-either:
- pattern: "tls.Config{..., MinVersion: tls.VersionTLS10, ...}"
- pattern: "tls.Config{..., MinVersion: tls.VersionTLS11, ...}"
- pattern: "tls.Config{..., MinVersion: 0x0301, ...}"
- pattern: "tls.Config{..., MinVersion: 0x0302, ...}"

- id: go-exec-command-inherits-environ-default
message: >-
`exec.Command(...).Run()` or `.Output()` without setting `cmd.Env`
inherits the parent process environment — in a CI runner that
includes every secret injected as an env var. Pass an explicit
filtered `cmd.Env = []string{...}`. The standard pattern is
`cmd.Env = append(os.Environ()[:0:0], "PATH=" + os.Getenv("PATH"))`.
severity: WARNING
languages: [go]
paths:
include: ["**/*.go"]
exclude:
- "**/*_test.go"
# pkg/security/{sbom,scan,tools} is the SC binary's intentional
# design surface for invoking external scanners (syft, trivy,
# grype) and installing toolchain — env inheritance is required
# so the scanners pick up PATH / HOME / GOPATH. These are not
# security-decision call sites.
- "pkg/security/sbom/**/*.go"
- "pkg/security/scan/**/*.go"
- "pkg/security/tools/**/*.go"
# Terminal control invocations of stty have no env-relevant
# impact and should not be flagged.
- "pkg/assistant/chat/input.go"
# git-status / git-remote read-only invocations need GIT_*
# env propagation to honor user gitconfig.
- "pkg/assistant/analysis/git_analyzer.go"
- "pkg/assistant/cicd/utils.go"
patterns:
- pattern-either:
- pattern: |
$CMD := exec.Command($ARG, ...)
...
$CMD.Run()
- pattern: |
$CMD := exec.Command($ARG, ...)
...
$CMD.Output()
- pattern: |
$CMD := exec.CommandContext($CTX, $ARG, ...)
...
$CMD.Run()
- pattern-not: |
$CMD := exec.Command(...)
...
$CMD.Env = ...
...
- pattern-not: |
$CMD := exec.CommandContext(...)
...
$CMD.Env = ...
...
# Skip trusted-CLI invocations: these require env propagation by
# design (git needs HOME/GIT_*; stty needs TERM; gh needs GH_TOKEN;
# cosign/syft/trivy/grype need PATH). Threat-model is about
# untrusted-binary invocation, not all exec.Command usage.
- metavariable-pattern:
metavariable: $ARG
patterns:
- pattern-regex: '.'
- pattern-not-regex: '"(git|stty|gh|cosign|syft|trivy|grype|kubectl|docker|pulumi|sc|welder)"'
93 changes: 93 additions & 0 deletions semgrep-scan/rules/pulumi-iac.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Pulumi-Go IaC hardening — SC provisions parent/client stacks on AWS,
# GCP, and Kubernetes. The existing SC ruleset only covers AWS RDS
# encryption. These rules expand to GCP + Kubernetes baselines.
# Ports of Checkov / Kubesec patterns to Pulumi-Go's API surface.

rules:
- id: go-pulumi-k8s-privileged-workload
message: >-
Pulumi-Kubernetes Pod/Container is provisioned with a privilege-
escalation knob (`Privileged: true`, `HostNetwork: true`,
`HostPID: true`, or a hostPath volume mount). On a multi-tenant
cluster this allows breakout to the node. Drop the privilege OR
document the workload as cluster-singleton with an explicit
threat-model justification.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
pattern-either:
- pattern: |
$ARGS = corev1.SecurityContextArgs{..., Privileged: pulumi.Bool(true), ...}
- pattern: |
$ARGS = corev1.PodSpecArgs{..., HostNetwork: pulumi.Bool(true), ...}
- pattern: |
$ARGS = corev1.PodSpecArgs{..., HostPID: pulumi.Bool(true), ...}
- pattern: |
$V = corev1.VolumeArgs{..., HostPath: $HP, ...}

- id: go-pulumi-gcp-storage-public-access
message: >-
GCP Storage Bucket / BucketIAMMember allows public-internet read
(`allUsers` or `allAuthenticatedUsers` member) OR disables
Uniform Bucket-Level Access. Even if the bucket holds non-sensitive
data the public-ACL surface is a CIS GCP audit failure (CKV_GCP_28,
CKV_GCP_29). Use a private bucket + signed URLs for public access.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
pattern-either:
- pattern: |
$M = storage.BucketIAMMemberArgs{..., Member: pulumi.String("allUsers"), ...}
- pattern: |
$M = storage.BucketIAMMemberArgs{..., Member: pulumi.String("allAuthenticatedUsers"), ...}
- pattern: |
$B = storage.BucketArgs{..., UniformBucketLevelAccess: pulumi.Bool(false), ...}

- id: go-pulumi-gcp-compute-default-sa-cloud-platform
message: >-
GCE VM uses the default Compute Engine service account paired with
the broad `cloud-platform` OAuth scope. An attacker with RCE on
the VM can call ANY GCP API as the default SA — full project
takeover. Bind the VM to a least-privilege custom service account
and use narrow scopes (or `cloud-platform` only on the custom SA).
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
patterns:
- pattern: |
$A = compute.InstanceArgs{..., ServiceAccount: $SA, ...}
- pattern-regex: 'compute\.InstanceArgs\{[\s\S]{0,400}ServiceAccount:[\s\S]{0,200}Scopes:[\s\S]{0,200}cloud-platform'

- id: go-pulumi-aws-sg-open-ingress
message: >-
AWS SecurityGroup or SecurityGroupRule with ingress `CidrBlocks`
containing `0.0.0.0/0` (any source). For non-HTTPS / non-HTTP
protocols this is a CIS AWS Foundation control failure (CKV_AWS_24
/ CKV_AWS_260). Restrict to the source CIDR the workload actually
needs.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
# Regex-only: the AST shape `ec2.SecurityGroupArgs{Ingress: ...}`
# matches ANY ingress (including restricted CIDRs), so the original
# AST alternative was dropped in favor of the precise CIDR-string
# regex below.
pattern-regex: 'CidrBlocks:[\s]*pulumi\.StringArray\{[^\}]*"0\.0\.0\.0/0"'

- id: go-pulumi-iam-wildcard-policy
message: >-
IAM policy statement with `Action: "*"` and/or `Resource: "*"`.
Grants god-mode access — never appropriate outside narrowly-scoped
break-glass automation. Document or split into least-privilege
grants.
severity: ERROR
languages: [go]
paths:
include: ["**/*.go"]
pattern-either:
- pattern-regex: '"Action":\s*"\*"'
- pattern-regex: '"Resource":\s*"\*"[^\}]*"Effect":\s*"Allow"'
Loading