Skip to content
Open
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
4 changes: 3 additions & 1 deletion iac/ql/lib/codeql/iac/YamlDocumentClassification.qll
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import iac
private import codeql.iac.YAML
private import codeql.iac.azure.Pipelines
private import codeql.iac.helmcharts.HelmChart
private import codeql.iac.kubernetes.Kubernetes
private import codeql.iac.compose.Compose
private import codeql.iac.openapi.OpenApi
private import codeql.iac.aws.CloudFormation
Expand Down Expand Up @@ -69,7 +70,8 @@ module YamlDocumentClassification {
(
doc instanceof AzurePipelines::Document and kind = "ado-pipeline"
or
doc instanceof HelmChart::Document and kind = "kubernetes-helm"
(doc instanceof HelmChart::Document or doc instanceof YamlKubernetes::Document) and
kind = "kubernetes-helm"
or
doc instanceof Compose::Document and kind = "compose"
or
Expand Down
75 changes: 73 additions & 2 deletions iac/ql/lib/codeql/iac/compose/Compose.qll
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ module Compose {
* Returns the services defined in the Compose file.
*/
Service getServices() { result = this.lookup("services").getAChildNode() }

YamlValue getNetworks() { result = this.lookup("networks") }

YamlValue getVolumes() { result = this.lookup("volumes") }

YamlValue getSecrets() { result = this.lookup("secrets") }
}

/**
Expand All @@ -51,8 +57,73 @@ module Compose {
* Returns the name of the service.
*/
string getName() {
result = this.lookup("container_name").toString()
// TODO get parent key name
result = yamlToString(this.lookup("container_name"))
or
exists(YamlMapping services, YamlValue key, YamlValue value |
services = compose.lookup("services") and
services.maps(key, value) and
value = this and
result = key.toString()
)
}

string getImage() { result = yamlToString(this.lookup("image")) }

YamlValue getBuild() { result = this.lookup("build") }

YamlValue getEnvironment() { result = this.lookup("environment") }

EnvironmentEntry getEnvironmentEntries() {
result = this.lookup("environment").(YamlSequence).getAChild()
or
result = this.lookup("environment").(YamlMapping).getAChild()
}

YamlValue getSecrets() { result = this.lookup("secrets") }

YamlValue getVolumes() { result = this.lookup("volumes") }

YamlValue getCapAdd() { result = this.lookup("cap_add") }

YamlValue getCapDrop() { result = this.lookup("cap_drop") }

YamlValue getPrivileged() { result = this.lookup("privileged") }

YamlValue getReadOnly() { result = this.lookup("read_only") }

YamlValue getUser() { result = this.lookup("user") }

YamlValue getPid() { result = this.lookup("pid") }

YamlValue getNetworkMode() { result = this.lookup("network_mode") }

YamlValue getDevices() { result = this.lookup("devices") }
}

class EnvironmentEntry extends YamlValue {
EnvironmentEntry() {
exists(Service service |
service.lookup("environment").(YamlSequence).getAChild() = this
)
or
exists(Service service |
service.lookup("environment").(YamlMapping).getAChild() = this
)
}

string getName() {
exists(YamlMapping environment, YamlValue key, YamlValue value |
environment.maps(key, value) and
value = this and
result = yamlToString(key.(YamlString))
)
or
result = this.(YamlString).getValue().regexpCapture("([^=]+)=.*", 1)
or
result = this.(YamlString).getValue() and
not result.matches("%=%")
}

YamlValue getValue() { result = this }
}
}
227 changes: 227 additions & 0 deletions iac/ql/lib/codeql/iac/kubernetes/Kubernetes.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
private import codeql.iac.YAML
private import codeql.files.FileSystem

/**
* Structural model for Kubernetes YAML manifests.
*
* The model intentionally covers locally observable workload and RBAC
* structure. It does not attempt to resolve admission policies, rendered Helm
* values, or cluster-level defaults.
*/
module YamlKubernetes {
class Document extends YamlNode, YamlDocument, YamlMapping {
Document() {
this.getFile().getExtension() = ["yml", "yaml"] and
exists(this.lookup("apiVersion")) and
exists(this.lookup("kind")) and
this.lookup("kind").(YamlString).getValue() != "Chart"
}

override string toString() { result = "Kubernetes " + this.getKind() + " document" }

string getApiVersion() { result = yamlToString(this.lookup("apiVersion")) }

string getKind() { result = yamlToString(this.lookup("kind")) }

Metadata getMetadata() { result = this.lookup("metadata") }

YamlMapping getSpec() { result = this.lookup("spec") }

PodSpec getPodSpec() { result.getDocument() = this }

Container getContainers() { result = this.getPodSpec().getContainers() }

Container getInitContainers() { result = this.getPodSpec().getInitContainers() }

Container getEphemeralContainers() { result = this.getPodSpec().getEphemeralContainers() }
}

class Metadata extends YamlNode, YamlMapping {
Metadata() { exists(Document document | document.lookup("metadata") = this) }

string getName() { result = yamlToString(this.lookup("name")) }

string getNamespace() { result = yamlToString(this.lookup("namespace")) }

YamlValue getLabels() { result = this.lookup("labels") }

YamlValue getAnnotations() { result = this.lookup("annotations") }
}

class PodSpec extends YamlNode, YamlMapping {
PodSpec() {
exists(Document document |
document.getKind() = "Pod" and
document.lookup("spec") = this
)
or
exists(Document document |
document.getKind() =
[
"DaemonSet", "Deployment", "Job", "ReplicaSet", "ReplicationController", "StatefulSet",
] and
document.lookup("spec").(YamlMapping).lookup("template").(YamlMapping).lookup("spec") = this
)
or
exists(Document document |
document.getKind() = "CronJob" and
document.lookup("spec").(YamlMapping).lookup("jobTemplate").(YamlMapping).lookup("spec")
.(YamlMapping).lookup("template").(YamlMapping).lookup("spec") = this
)
}

Container getContainers() { result = this.lookup("containers").(YamlSequence).getAChild() }

Container getInitContainers() {
result = this.lookup("initContainers").(YamlSequence).getAChild()
}

Container getEphemeralContainers() {
result = this.lookup("ephemeralContainers").(YamlSequence).getAChild()
}

SecurityContext getSecurityContext() { result = this.lookup("securityContext") }

YamlValue getServiceAccountName() { result = this.lookup("serviceAccountName") }

YamlValue getAutomountServiceAccountToken() {
result = this.lookup("automountServiceAccountToken")
}

Volume getVolumes() { result = this.lookup("volumes").(YamlSequence).getAChild() }

YamlValue getHostNetwork() { result = this.lookup("hostNetwork") }

YamlValue getHostPid() { result = this.lookup("hostPID") }

YamlValue getHostIpc() { result = this.lookup("hostIPC") }
}

class Container extends YamlNode, YamlMapping {
Container() {
exists(PodSpec pod | pod.lookup("containers").(YamlSequence).getAChildNode() = this)
or
exists(PodSpec pod | pod.lookup("initContainers").(YamlSequence).getAChildNode() = this)
or
exists(PodSpec pod | pod.lookup("ephemeralContainers").(YamlSequence).getAChildNode() = this)
}

string getName() { result = yamlToString(this.lookup("name")) }

string getImage() { result = yamlToString(this.lookup("image")) }

SecurityContext getSecurityContext() { result = this.lookup("securityContext") }

YamlValue getCommand() { result = this.lookup("command") }

YamlValue getArgs() { result = this.lookup("args") }

YamlValue getEnv() { result = this.lookup("env") }

EnvEntry getEnvironmentEntries() { result = this.lookup("env").(YamlSequence).getAChild() }

YamlValue getEnvFrom() { result = this.lookup("envFrom") }

YamlValue getVolumeMounts() { result = this.lookup("volumeMounts") }

YamlValue getPorts() { result = this.lookup("ports") }
}

class EnvEntry extends YamlNode, YamlMapping {
EnvEntry() {
exists(Container container | container.lookup("env").(YamlSequence).getAChildNode() = this)
}

string getName() { result = yamlToString(this.lookup("name")) }

YamlValue getValue() { result = this.lookup("value") }

YamlMapping getValueFrom() { result = this.lookup("valueFrom") }

YamlMapping getSecretKeyRef() { result = this.getValueFrom().lookup("secretKeyRef") }

YamlMapping getConfigMapKeyRef() { result = this.getValueFrom().lookup("configMapKeyRef") }
}

class SecurityContext extends YamlNode, YamlMapping {
SecurityContext() {
exists(PodSpec pod | pod.lookup("securityContext") = this)
or
exists(Container container | container.lookup("securityContext") = this)
}

YamlValue getPrivileged() { result = this.lookup("privileged") }

YamlValue getAllowPrivilegeEscalation() {
result = this.lookup("allowPrivilegeEscalation")
}

YamlValue getRunAsUser() { result = this.lookup("runAsUser") }

YamlValue getRunAsGroup() { result = this.lookup("runAsGroup") }

YamlValue getRunAsNonRoot() { result = this.lookup("runAsNonRoot") }

YamlValue getReadOnlyRootFilesystem() {
result = this.lookup("readOnlyRootFilesystem")
}

YamlValue getCapabilities() { result = this.lookup("capabilities") }

YamlValue getSeccompProfile() { result = this.lookup("seccompProfile") }
}

class Volume extends YamlNode, YamlMapping {
Volume() { exists(PodSpec pod | pod.lookup("volumes").(YamlSequence).getAChildNode() = this) }

string getName() { result = yamlToString(this.lookup("name")) }

YamlValue getHostPath() { result = this.lookup("hostPath") }

YamlValue getProjected() { result = this.lookup("projected") }

YamlValue getSecret() { result = this.lookup("secret") }

YamlValue getConfigMap() { result = this.lookup("configMap") }
}

class Role extends Document {
Role() { this.getKind() = ["Role", "ClusterRole"] }

Rule getRules() { result = this.lookup("rules").(YamlSequence).getAChild() }
}

class Rule extends YamlNode, YamlMapping {
Rule() { exists(Role role | role.lookup("rules").(YamlSequence).getAChildNode() = this) }

YamlValue getApiGroups() { result = this.lookup("apiGroups") }

YamlValue getResources() { result = this.lookup("resources") }

YamlValue getVerbs() { result = this.lookup("verbs") }

YamlValue getResourceNames() { result = this.lookup("resourceNames") }
}

class RoleBinding extends Document {
RoleBinding() { this.getKind() = ["RoleBinding", "ClusterRoleBinding"] }

YamlMapping getRoleRef() { result = this.lookup("roleRef") }

Subject getSubjects() { result = this.lookup("subjects").(YamlSequence).getAChild() }
}

class Subject extends YamlNode, YamlMapping {
Subject() {
exists(RoleBinding binding |
binding.lookup("subjects").(YamlSequence).getAChildNode() = this
)
}

string getKind() { result = yamlToString(this.lookup("kind")) }

string getName() { result = yamlToString(this.lookup("name")) }

string getNamespace() { result = yamlToString(this.lookup("namespace")) }
}
}
3 changes: 3 additions & 0 deletions iac/ql/lib/iac.qll
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import codeql.iac.containers.Containers
import codeql.iac.containers.Images
// Compose
import codeql.iac.compose.Compose
// Kubernetes
import codeql.iac.kubernetes.Kubernetes
// HelmCharts
import codeql.iac.helmcharts.HelmChart
// Terraform / HCL
Expand All @@ -20,3 +22,4 @@ import hcl
import codeql.iac.openapi.OpenApi
// YAML
import codeql.iac.YAML
import codeql.iac.YamlDocumentClassification
13 changes: 13 additions & 0 deletions iac/ql/test/library-tests/compose/ast/AST.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
documents
| compose.yaml:1:1:18:25 | services: |
| docker-compose.yaml:1:1:11:29 | services: |
services
| compose.yaml:3:5:15:23 | image: nginx:1.25 |
| docker-compose.yaml:3:5:8:21 | image: ... ker:1.0 |
environment
| compose.yaml:5:7:5:14 | PASSWORD |
| compose.yaml:5:17:5:27 | ${PASSWORD} |
| compose.yaml:6:7:6:11 | DEBUG |
| compose.yaml:6:14:6:19 | "true" |
| docker-compose.yaml:5:9:5:18 | DEBUG=true |
| docker-compose.yaml:6:9:6:17 | API_TOKEN |
5 changes: 5 additions & 0 deletions iac/ql/test/library-tests/compose/ast/AST.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
private import iac

query predicate documents(Compose::Document n) { any() }
query predicate services(Compose::Service n) { any() }
query predicate environment(Compose::EnvironmentEntry n) { any() }
18 changes: 18 additions & 0 deletions iac/ql/test/library-tests/compose/ast/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
web:
image: nginx:1.25
environment:
PASSWORD: ${PASSWORD}
DEBUG: "true"
secrets:
- app-password
cap_drop:
- ALL
read_only: true
user: "1000:1000"
network_mode: bridge
volumes:
- ./web:/srv/web
secrets:
app-password:
file: ./password.txt
11 changes: 11 additions & 0 deletions iac/ql/test/library-tests/compose/ast/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
worker:
image: example/worker:1.0
environment:
- DEBUG=true
- API_TOKEN
secrets:
- worker-token
secrets:
worker-token:
file: ./worker-token.txt
Loading
Loading