rules

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package rules provides the built-in lint rules bundled with decolint. See linter.Rule for the fields a rule declares.

To add a new rule, declare a linter.Rule value in a new file in this package and register it in RegisterRules.

Index

Constants

This section is empty.

Variables

View Source
var ConflictingContainerDef = &linter.Rule{
	ID:          "conflicting-container-def",
	Description: `disallow a devcontainer.json that defines more than one of "image", "build", or "dockerComposeFile"`,
	LongDescription: `The specification defines three mutually exclusive ways to create the container: from an image, from a
Dockerfile, or from a Docker Compose project. Which one wins when several are set is unspecified, so the
container that gets built depends on the tool rather than on the configuration. Keep the variant the
project actually uses and remove the others.`,
	References: []string{
		`https://containers.dev/implementors/spec/#orchestration-options`,
		`https://containers.dev/implementors/json_reference/#scenario-specific`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "build": {
    "dockerfile": "Dockerfile"
  }
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "build": {
    "dockerfile": "Dockerfile"
  }
}
`},
			},
		},
	},
	Check: checkConflictingContainerDef,
}

ConflictingContainerDef reports a devcontainer.json that defines more than one of "image", "build", or "dockerComposeFile". The schema treats these container-definition variants as mutually exclusive, so exactly one may be set.

View Source
var FeatureInstallScriptNotExecutable = &linter.Rule{
	ID:          "feature-install-script-not-executable",
	Description: "disallow a Feature's `install.sh` that lacks executable permission bits",
	LongDescription: `The specification has the installing tool invoke "install.sh" directly rather than through a shell, so
that the script's own shebang selects the interpreter. That requires the execute bit: without it the
Feature fails to install when a container is built. Run "chmod +x install.sh" and commit the mode change.`,
	References: []string{
		`https://containers.dev/implementors/features/#invoking-installsh`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Feature},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature},
				{Path: installScriptName, Content: featureInstallScriptExampleScript, Mode: 0o644},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature},
				{Path: installScriptName, Content: featureInstallScriptExampleScript, Mode: 0o755},
			},
		},
		Note: "Git records the executable bit, so committing the mode change is what makes\n" +
			"the fix stick. On Windows, where the filesystem has no executable bit, set it\n" +
			"in the index directly: `git update-index --chmod=+x install.sh`.",
	},
	Check: checkFeatureInstallScriptNotExecutable,
}

FeatureInstallScriptNotExecutable reports a Feature whose install.sh exists but carries no executable permission bit, so the container runtime cannot run it. Reporting a missing install.sh is MissingFeatureInstallScript's job; the two never fire together.

View Source
var IDDirMismatch = &linter.Rule{
	ID:          "id-dir-mismatch",
	Description: `disallow a Feature's or Template's "id" that does not match the name of its containing directory`,
	LongDescription: `Both specifications require the "id" to match the name of the directory holding the metadata file, since
that directory name is what packaging and distribution address the artifact by. When the two disagree the
published reference does not resolve to what the directory contains; rename the directory or the "id" so
they agree.`,
	References: []string{
		`https://containers.dev/implementors/features/#devcontainer-feature-json-properties`,
		`https://containers.dev/implementors/templates/#devcontainer-templatejson-properties`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Feature, linter.Template},
	Paths:     []string{"/id"},
	Example: linter.Example{
		Bad: linter.Snippet{
			DirName: "node",
			Files: []linter.ExampleFile{
				{Path: `devcontainer-feature.json`, Content: `// src/node/devcontainer-feature.json
{
  "id": "nodejs",
  "version": "1.0.0",
  "name": "Node.js"
}
`},
			},
		},
		Good: linter.Snippet{
			DirName: "node",
			Files: []linter.ExampleFile{
				{Path: `devcontainer-feature.json`, Content: `// src/node/devcontainer-feature.json
{
  "id": "node",
  "version": "1.0.0",
  "name": "Node.js"
}
`},
			},
		},
	},
	Check: checkIDDirMismatch,
}

IDDirMismatch reports a Feature's or Template's "id" property when it does not match the name of the directory containing its metadata file, per the Dev Container Features/Templates convention.

View Source
var InvalidSemver = &linter.Rule{
	ID:          "invalid-semver",
	Description: `disallow a Feature's or Template's "version" that is not a valid semantic version`,
	LongDescription: `Publishing a Feature or Template pushes it under tags derived from the "version" components: the full
version, "major.minor", and "major", so consumers can pin as loosely or as tightly as they want. A value
that is not valid semver has no such components, leaving nothing to derive those tags from.`,
	References: []string{
		`https://containers.dev/implementors/features-distribution/#versioning`,
		`https://semver.org/`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Feature, linter.Template},
	Paths:     []string{"/version"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer-feature.json`, Content: `{
  "id": "node",
  "version": "1.0",
  "name": "Node.js"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer-feature.json`, Content: `{
  "id": "node",
  "version": "1.0.0",
  "name": "Node.js"
}
`},
			},
		},
	},
	Check: checkInvalidSemver,
}

InvalidSemver reports a Feature's or Template's "version" property when its value is not a valid semantic version, per the Dev Container Features/Templates specification.

View Source
var MissingBuildDockerfile = &linter.Rule{
	ID:          "missing-build-dockerfile",
	Description: `disallow a devcontainer.json "build" object that is missing "dockerfile"`,
	LongDescription: `"build.dockerfile" is the only required member of "build": it locates, relative to the devcontainer.json,
the Dockerfile the image is built from. The other members ("context", "args", "target", ...) only shape a
build that "dockerfile" defines, so without it there is nothing to build.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#image-specific`,
		`https://containers.dev/implementors/spec/#dockerfile-based`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{"/build"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "build": {
    "context": ".."
  }
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  }
}
`},
			},
		},
	},
	Check: checkMissingBuildDockerfile,
}

MissingBuildDockerfile reports a devcontainer.json "build" object that does not set "dockerfile", leaving no way to know which Dockerfile to build.

View Source
var MissingComposeService = &linter.Rule{
	ID:          "missing-compose-service",
	Description: `disallow a devcontainer.json that sets "dockerComposeFile" without "service"`,
	LongDescription: `A Compose project usually defines several services, so naming the Compose file does not say which
container the tooling should attach to. The specification requires "service" to name that main container:
it is the one lifecycle scripts run in and the one editors connect to.`,
	References: []string{
		`https://containers.dev/implementors/spec/#docker-compose-based`,
		`https://containers.dev/implementors/json_reference/#compose-specific`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "dockerComposeFile": "docker-compose.yml",
  "workspaceFolder": "/workspace"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace"
}
`},
			},
		},
	},
	Check: checkMissingComposeService,
}

MissingComposeService reports a devcontainer.json that sets "dockerComposeFile" without also setting "service", leaving the tool no way to know which compose service to attach to.

View Source
var MissingContainerDef = &linter.Rule{
	ID:          "missing-container-def",
	Description: `disallow a devcontainer.json that defines none of "image", "build", or "dockerComposeFile"`,
	LongDescription: `Every dev container is created from exactly one of "image", "build", or "dockerComposeFile", and each of
the three is required in its own scenario. A configuration that sets none of them describes no container
at all, so no tool can create one from it.`,
	References: []string{
		`https://containers.dev/implementors/spec/#orchestration-options`,
		`https://containers.dev/implementors/json_reference/#scenario-specific`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "forwardPorts": [3000]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "name": "my project",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "forwardPorts": [3000]
}
`},
			},
		},
	},
	Check: checkMissingContainerDef,
}

MissingContainerDef reports a devcontainer.json that defines none of "image", "build", or "dockerComposeFile", leaving no way to build a container.

View Source
var MissingFeatureInstallScript = &linter.Rule{
	ID:          "missing-feature-install-script",
	Description: "disallow a Feature directory without the required `install.sh` install script",
	LongDescription: `A Feature is distributed as its metadata file plus the "install.sh" the tooling runs inside the container,
which is where the Feature does all of its work. A directory without one publishes a Feature that
installs nothing, and the omission only surfaces when someone builds a container with it.`,
	References: []string{
		`https://containers.dev/implementors/features/#folder-structure`,
		`https://containers.dev/implementors/features/#invoking-installsh`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Feature},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature},
				{Path: installScriptName, Content: featureInstallScriptExampleScript, Mode: 0o755},
			},
		},
		Note: "The name is fixed: the tooling runs `install.sh` and nothing else, so an\n" +
			"install script under any other name is never executed.",
	},
	Check: checkMissingFeatureInstallScript,
}

MissingFeatureInstallScript reports a Feature whose directory has no install.sh install script, the entrypoint the Features specification requires alongside devcontainer-feature.json.

View Source
var MissingRequiredProps = &linter.Rule{
	ID:          "missing-required-props",
	Description: `disallow a Feature's or Template's metadata that is missing a required property ("id", "version", or "name")`,
	LongDescription: `"id", "version", and "name" are the only properties either specification requires: the "id" addresses the
artifact, the "version" is what consumers pin to, and the "name" is what a user recognizes it by in a
list. Metadata missing any of them cannot be published as a usable Feature or Template.`,
	References: []string{
		`https://containers.dev/implementors/features/#devcontainer-feature-json-properties`,
		`https://containers.dev/implementors/templates/#devcontainer-templatejson-properties`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Feature, linter.Template},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer-feature.json`, Content: `{
  "id": "node",
  "version": "1.0.0"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer-feature.json`, Content: `{
  "id": "node",
  "version": "1.0.0",
  "name": "Node.js"
}
`},
			},
		},
	},
	Check: checkMissingRequiredProps,
}

MissingRequiredProps reports a Feature's or Template's metadata when it is missing a required property ("id", "version", or "name").

View Source
var MissingWorkspaceMountFolder = &linter.Rule{
	ID:          "missing-workspace-mount-folder",
	Description: `disallow a devcontainer.json using "image" or "build" that sets only one of "workspaceMount" or "workspaceFolder"`,
	LongDescription: `The two properties describe opposite ends of the same override: "workspaceMount" says where the source
code is mounted, "workspaceFolder" says which path inside the container the tooling opens. The reference
documents each as requiring the other, because setting one alone either mounts the source somewhere
nothing opens, or opens a path nothing is mounted at.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#image-specific`,
		`https://containers.dev/implementors/spec/#workspace-folder`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "workspaceMount": "source=${localWorkspaceFolder},target=/srv/app,type=bind"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "workspaceMount": "source=${localWorkspaceFolder},target=/srv/app,type=bind",
  "workspaceFolder": "/srv/app"
}
`},
			},
		},
	},
	Check: checkMissingWorkspaceMountFolder,
}

MissingWorkspaceMountFolder reports a devcontainer.json that uses "image" or "build" and sets only one of "workspaceMount" or "workspaceFolder", leaving the tool unable to tell where the overridden mount lands inside the container.

View Source
var NoAppPort = &linter.Rule{
	ID:          "no-app-port",
	Description: `disallow the legacy "appPort" property in favor of "forwardPorts"`,
	LongDescription: `"appPort" publishes the port the way Docker does: it is fixed when the container is created, and the
application has to listen on all interfaces rather than just "localhost" to be reachable. A forwarded
port instead looks like "localhost" to the application and can be changed without recreating the
container, which is why the reference recommends "forwardPorts" in most cases.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#image-specific`,
		`https://containers.dev/implementors/json_reference/#publishing-vs-forwarding-ports`,
	},
	Category:  linter.CategoryStyle,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{"/appPort"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "appPort": [3000]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "forwardPorts": [3000]
}
`},
			},
		},
	},
	Check: checkNoAppPort,
}

NoAppPort reports the legacy "appPort" property. It only supports statically publishing ports at container-creation time; "forwardPorts" is the modern replacement and forwards ports dynamically without requiring the container to be recreated.

View Source
var NoApparmorUnconfined = &linter.Rule{
	ID:          "no-apparmor-unconfined",
	Description: `disallow disabling AppArmor confinement via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt apparmor=unconfined" entry in a devcontainer.json's "runArgs"`,
	LongDescription: `A container runtime applies its own AppArmor profile ("docker-default" for Docker) to every container on
a host that has AppArmor enabled, restricting what the container may do to the host's filesystem,
capabilities, and network. "apparmor=unconfined" removes that profile outright, so the only thing left
between a process in the container and the host is the discretionary access control the container's user
is already subject to. The setting is usually copied from instructions for running nested containers or a
debugger, both of which have narrower settings that work.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/apparmor/`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature},
	Paths:     []string{"/securityOpt/*", "/runArgs/--security-opt"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "securityOpt": ["apparmor=unconfined"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_PTRACE"]
}
`},
			},
		},
	},
	Check: checkNoApparmorUnconfined,
}

NoApparmorUnconfined reports a devcontainer.json or devcontainer-feature.json that disables AppArmor confinement, either via the "securityOpt" property or, in a devcontainer.json, a "--security-opt apparmor=unconfined" entry in "runArgs". It is the AppArmor counterpart of NoSeccompUnconfined: both remove a mandatory confinement layer the runtime applies by default.

View Source
var NoBindMount = &linter.Rule{
	ID:          "no-bind-mount",
	Description: `disallow "bind" type entries in "mounts", which GitHub Codespaces silently ignores except for the Docker socket`,
	LongDescription: `A codespace runs on a machine in the cloud, where the host path a bind mount points at does not exist, so
Codespaces documents that it ignores "bind" mounts apart from the Docker socket. The mount is dropped
without an error and the container starts missing the data it expects. Volume mounts are honored, so use
"type=volume" for anything that only has to persist across rebuilds.`,
	References: []string{
		`https://containers.dev/supporting#codespaces-specific-limitations`,
		`https://containers.dev/implementors/spec/#mounts`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Platforms: []linter.Platform{linter.PlatformCodespaces},
	Paths:     []string{"/mounts/*"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "mounts": [
    {
      "source": "${localWorkspaceFolder}/.cache",
      "target": "/home/vscode/.cache",
      "type": "bind"
    }
  ]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "mounts": [
    {
      "source": "devcontainer-cache",
      "target": "/home/vscode/.cache",
      "type": "volume"
    }
  ]
}
`},
			},
		},
	},
	Check: checkNoBindMount,
}

NoBindMount reports "mounts" entries that use the "bind" mount type. The Dev Container spec allows bind mounts, but GitHub Codespaces silently ignores them, except for a mount whose source is the Docker socket.

View Source
var NoCapAddAll = &linter.Rule{
	ID:          "no-cap-add-all",
	Description: `disallow granting all Linux capabilities via an "ALL" entry in the "capAdd" property, or a "--cap-add=ALL" entry in a devcontainer.json's "runArgs"`,
	LongDescription: `Linux capabilities split root's powers into units a container can be granted individually, and the runtime
withholds the dangerous ones by default. "ALL" hands them all over, including capabilities such as
"SYS_ADMIN" and "SYS_MODULE" that let a process reconfigure the host kernel and escape the container.
"capAdd" exists to name the one or two a workload actually needs, e.g. "SYS_PTRACE" for a debugger.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/#linux-kernel-capabilities`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature},
	Paths:     []string{"/capAdd/*", "/runArgs/--cap-add"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["ALL"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_PTRACE"]
}
`},
			},
		},
	},
	Check: checkNoCapAddAll,
}

NoCapAddAll reports a devcontainer.json or devcontainer-feature.json that grants every Linux capability to the container, either via an "ALL" entry in the "capAdd" property or, in a devcontainer.json, a "--cap-add=ALL" entry in "runArgs". Granting all capabilities gives the container far more privilege than most workloads need, which is a significant security risk.

View Source
var NoDangerousCapAdd = &linter.Rule{
	ID:          "no-dangerous-cap-add",
	Description: `disallow granting a Linux capability that lets a process act on the host, e.g. "SYS_ADMIN" or "SYS_MODULE", via the "capAdd" property or a "--cap-add" entry in a devcontainer.json's "runArgs"`,
	LongDescription: `Container runtimes withhold the capabilities that let a process act on the host rather than on the
container, and "capAdd" adds them back one at a time. Each capability this rule reports is on its own
enough to reach past the container — loading a module into the host kernel, opening a file by handle
outside the mounted filesystem, rebooting the machine — and none of them is granted by default, so one
that appears here was asked for. Grant only what the workload actually fails without.

A capability the kernel confines to the container's own namespaces is not reported, however privileged
it sounds: "SYS_PTRACE" reaches no further than the container's process namespace, and "NET_ADMIN" no
further than its network namespace. What makes those dangerous is sharing the host's namespaces, which
is a separate rule.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/#linux-kernel-capabilities`,
		`https://man7.org/linux/man-pages/man7/capabilities.7.html`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature},
	Paths:     []string{"/capAdd/*", "/runArgs/--cap-add"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_ADMIN"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_PTRACE"]
}
`},
			},
		},
	},
	Check: checkNoDangerousCapAdd,
}

NoDangerousCapAdd reports a devcontainer.json or devcontainer-feature.json that grants a Linux capability which lets a process reach past the container, either via the "capAdd" property or, in a devcontainer.json, a "--cap-add" entry in "runArgs". Unlike NoCapAddAll, which only flags granting every capability at once, this rule flags the individual capabilities in dangerousCapabilities.

View Source
var NoDockerSocketMount = &linter.Rule{
	ID:          "no-docker-socket-mount",
	Description: `disallow bind-mounting the host's Docker socket via a devcontainer.json's "mounts" or "runArgs", which grants the container root-equivalent control over the host`,
	LongDescription: `The Docker socket is the daemon's full API, and the daemon runs as root on the host. Anything that can
reach the socket can start a container that mounts the host's filesystem, so mounting it into the dev
container hands root-equivalent control of the host to every process inside — including code the
project's own build fetches. When the container genuinely needs Docker, a Docker-in-Docker Feature or a
rootless daemon keeps that access inside the container.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/#docker-daemon-attack-surface`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{"/mounts/*", "/runArgs/--mount", "/runArgs/--volume"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "mounts": [
    {
      "source": "/var/run/docker.sock",
      "target": "/var/run/docker.sock",
      "type": "bind"
    }
  ]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2.13.0": {}
  }
}
`},
			},
		},
	},
	Check: checkNoDockerSocketMount,
}

NoDockerSocketMount reports a devcontainer.json that bind-mounts the host's Docker daemon socket into the container, either via a "mounts" entry or a "-v"/"--volume"/"--mount" entry in "runArgs". Anything with access to the socket can control the host's Docker daemon, which is effectively root-equivalent access to the host.

View Source
var NoHostNamespace = &linter.Rule{
	ID:          "no-host-namespace",
	Description: `disallow sharing one of the host's namespaces with the container via a "--network=host", "--pid=host", "--ipc=host", "--uts=host", "--userns=host", or "--cgroupns=host" entry in "runArgs"`,
	LongDescription: `Namespaces are what make a container a container: the process table, network stack, and IPC objects it
sees are its own. A "host" value in "runArgs" hands one of them back, and the container stops being
isolated in that dimension. With "--pid=host" every process on the machine is visible from inside the
container, and a root process there can signal or trace it; with "--network=host" the container reaches
every service bound to the host's loopback interface, including the ones that are only reachable there
because they trust anything local. Put the container on a user-defined Docker network, or forward the
port you need, instead of joining the host's namespace.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#image-specific`,
		`https://github.com/docker/docker-bench-security`,
		`https://man7.org/linux/man-pages/man7/namespaces.7.html`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     hostNamespacePaths,
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "runArgs": ["--network=host"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "runArgs": ["--network=devnet"]
}
`},
			},
		},
		Note: `The good example puts the container on a user-defined Docker network, so it can
reach other containers on it by name while keeping a network namespace of its own.`,
	},
	Check: checkNoHostNamespace,
}

NoHostNamespace reports a devcontainer.json whose "runArgs" put the container in one of the host's namespaces, e.g. "--network=host" or "--pid=host". Each such entry removes the isolation that namespace provides.

View Source
var NoHostPortFormat = &linter.Rule{
	ID:          "no-host-port-format",
	Description: `disallow "host:port" entries in "forwardPorts" and "portsAttributes", which GitHub Codespaces does not support`,
	LongDescription: `The "host:port" form forwards a port from another container in a Docker Compose project (e.g. "db:5432")
rather than from the primary one. Codespaces documents that it does not support that variation of either
property, so the entry is ignored there and the port is not forwarded. A bare port number, which refers
to the primary container, works everywhere.`,
	References: []string{
		`https://containers.dev/supporting#codespaces-specific-limitations`,
		`https://containers.dev/implementors/json_reference/#general-properties`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Platforms: []linter.Platform{linter.PlatformCodespaces},
	Paths:     []string{"/forwardPorts/*", "/portsAttributes/*"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "forwardPorts": ["db:5432"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "forwardPorts": [5432]
}
`},
			},
		},
	},
	Check: checkNoHostPortFormat,
}

NoHostPortFormat reports "forwardPorts" entries and "portsAttributes" keys written in "host:port" format. The Dev Container spec allows that format, but GitHub Codespaces only supports a bare port number in either property.

View Source
var NoImageLatest = &linter.Rule{
	ID:          "no-image-latest",
	Description: `disallow container images without an explicit tag or with the "latest" tag`,
	LongDescription: `A reference with no tag resolves to "latest", and "latest" is just the tag a publisher moves as they
release. Either way the configuration says "whatever is current", so the same devcontainer.json builds a
different environment next month, and a build that broke cannot be reproduced from the file alone. Name
the version the project was tested against.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#image-specific`,
	},
	Category:  linter.CategoryReproducibility,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{"/image"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:latest"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04"
}
`},
			},
		},
	},
	Check: checkNoImageLatest,
}

NoImageLatest reports the "image" property when it references a container image without an explicit tag or with the "latest" tag. Such references are not reproducible: the image they resolve to changes over time.

View Source
var NoPrivilegedContainer = &linter.Rule{
	ID:          "no-privileged-container",
	Description: `disallow running the container in privileged mode via the "privileged" property or a "--privileged" entry in "runArgs"`,
	LongDescription: `A privileged container gets every Linux capability, unconfined seccomp and LSM profiles, and access to all
host devices. That removes essentially every boundary between the container and the host, so any code
running in it — including a compromised dependency pulled in by the project's own build — can take over
the machine. Docker-in-Docker is the usual reason it is set; a Feature that provides it, or the specific
capabilities and devices the workload needs, is a far narrower grant.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/#docker-daemon-attack-surface`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature},
	Paths:     []string{"/privileged", "/runArgs/--privileged"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "privileged": true
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_PTRACE"]
}
`},
			},
		},
		Note: `The good example grants only the capability a debugger needs. Reach for the
narrowest grant that works: ` + "`" + `capAdd` + "`" + ` for a capability, ` + "`" + `--device` + "`" + ` in ` + "`" + `runArgs` + "`" + `
for a device, and the docker-in-docker Feature rather than privileged mode for
nested containers.`,
	},
	Check: checkNoPrivilegedContainer,
}

NoPrivilegedContainer reports a devcontainer.json or devcontainer-feature.json that runs the container in privileged mode, either via the "privileged" property or, in a devcontainer.json, a "--privileged" entry in "runArgs". Privileged mode disables the container's isolation from the host, which is a significant security risk.

View Source
var NoSeccompOverride = &linter.Rule{
	ID:          "no-seccomp-override",
	Description: `disallow overriding the container runtime's default seccomp profile via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt seccomp=..." entry in a devcontainer.json's "runArgs"`,
	LongDescription: `The runtime's default seccomp profile blocks the syscalls containers do not need, several of which have
featured in container escapes. Pointing "seccomp" at a profile of your own replaces that default
wholesale, and a hand-written profile is rarely reviewed as carefully or updated as the kernel gains new
syscalls. Keep the default unless the workload provably needs more, and review the replacement if it
does.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/seccomp/`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature},
	Paths:     []string{"/securityOpt/*", "/runArgs/--security-opt"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "securityOpt": ["seccomp=./seccomp.json"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_PTRACE"]
}
`},
			},
		},
		Note: `Leaving ` + "`" + `securityOpt` + "`" + ` unset keeps the runtime's default seccomp profile,
which already allows what a development container normally does.`,
	},
	Check: checkNoSeccompOverride,
}

NoSeccompOverride reports a devcontainer.json or devcontainer-feature.json that overrides the container runtime's default seccomp profile, either via the "securityOpt" property or, in a devcontainer.json, a "--security-opt seccomp=..." entry in "runArgs". Unlike NoSeccompUnconfined, which only flags disabling seccomp entirely, this rule flags any override, including a custom profile, since it replaces the runtime's vetted default. It is off by default because many projects legitimately ship a custom profile.

View Source
var NoSeccompUnconfined = &linter.Rule{
	ID:          "no-seccomp-unconfined",
	Description: `disallow disabling seccomp confinement via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt seccomp=unconfined" entry in a devcontainer.json's "runArgs"`,
	LongDescription: `"seccomp=unconfined" turns off syscall filtering entirely, exposing the whole kernel API — including the
calls the default profile blocks precisely because they have been used to break out of containers. The
setting is most often copied from debugger instructions, where granting the "SYS_PTRACE" capability is
enough on current runtimes.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/seccomp/`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature},
	Paths:     []string{"/securityOpt/*", "/runArgs/--security-opt"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "securityOpt": ["seccomp=unconfined"]
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "capAdd": ["SYS_PTRACE"]
}
`},
			},
		},
	},
	Check: checkNoSeccompUnconfined,
}

NoSeccompUnconfined reports a devcontainer.json or devcontainer-feature.json that disables seccomp confinement, either via the "securityOpt" property or, in a devcontainer.json, a "--security-opt seccomp=unconfined" entry in "runArgs". Running unconfined removes a key layer of kernel-level syscall filtering that isolates the container from the host.

View Source
var PinExtensionVersion = &linter.Rule{
	ID:          "pin-extension-version",
	Description: `disallow a "customizations.vscode.extensions" entry without an explicit pinned version`,
	LongDescription: `An extension ID on its own installs whatever the marketplace publishes at the moment the container is
created, so two developers on the same devcontainer.json can end up with different formatters, linters, or
language server versions — and an extension update can change the environment without any commit.
Appending a version (` + "`publisher.name@1.2.3`" + `) makes the editor tooling as pinned as the rest of the image.`,
	References: []string{
		`https://containers.dev/supporting#visual-studio-code`,
		`https://code.visualstudio.com/docs/configure/extensions/extension-marketplace`,
	},
	Category:  linter.CategoryReproducibility,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Platforms: []linter.Platform{linter.PlatformVSCode, linter.PlatformCodespaces},
	Paths:     []string{"/customizations/vscode/extensions/*"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "customizations": {
    "vscode": {
      "extensions": ["golang.go"]
    }
  }
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "customizations": {
    "vscode": {
      "extensions": ["golang.go@0.50.0"]
    }
  }
}
`},
			},
		},
	},
	Check: checkPinExtensionVersion,
}

PinExtensionVersion reports a "customizations.vscode.extensions" entry that does not pin an explicit version (e.g. "publisher.name@1.2.3"). Without a pinned version, the VS Code Dev Containers extension and GitHub Codespaces always install the latest published version, which is not reproducible.

View Source
var PinFeatureVersion = &linter.Rule{
	ID:          "pin-feature-version",
	Description: `disallow a Feature reference without an explicit version or with the "latest" version`,
	LongDescription: `A Feature reference with no version resolves to "latest", so the container installs whatever the Feature's
author published most recently — the tooling it sets up can change under the project without the
devcontainer.json changing at all. Features are published under their full version as well as
"major.minor" and "major" tags, so a reference can be pinned as tightly as the project wants.`,
	References: []string{
		`https://containers.dev/implementors/features-distribution/#versioning`,
		`https://containers.dev/implementors/features/#referencing-a-feature`,
	},
	Category:  linter.CategoryReproducibility,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{"/features"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/devcontainers/features/go": {}
  }
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/devcontainers/features/go:1.3.2": {}
  }
}
`},
			},
		},
	},
	Check: checkPinFeatureVersion,
}

PinFeatureVersion reports a "features" entry whose key references an OCI Feature without an explicit version tag or with the "latest" tag. Such references are not reproducible: the Feature they resolve to changes over time. Local path Features (e.g. "./my-feature") and direct tarball URIs (e.g. "https://.../devcontainer-feature.tgz") have no version tag to pin and are not checked.

View Source
var PinImageDigest = &linter.Rule{
	ID:          "pin-image-digest",
	Description: `disallow an "image" property that does not pin the image by content digest (e.g. "image@sha256:...")`,
	LongDescription: `A tag is a mutable pointer: the publisher can move even a fully specified one to different bits, and a
registry can serve a different image for the same tag on a different day. A digest names the content
itself, so "image@sha256:..." always resolves to the exact image the project was tested with, and the
client verifies what it pulled against it.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#image-specific`,
		`https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests`,
	},
	Category:  linter.CategoryReproducibility,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{"/image"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04@sha256:2a1d1e1a4b0c3f8e5c8a1e0a6d3b7c9f4e2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b"
}
`},
			},
		},
	},
	Check: checkPinImageDigest,
}

PinImageDigest reports the "image" property when it references a container image without a content digest (e.g. "ubuntu@sha256:..."). Unlike NoImageLatest, which only flags a missing or "latest" tag, this rule flags any reference that isn't pinned by digest, since even a fixed tag can later be reassigned to point at a different image. It is off by default because digest-pinning every image is a heavier requirement than most projects want.

View Source
var RequireCapDropAll = &linter.Rule{
	ID:          "require-cap-drop-all",
	Description: `require a "--cap-drop=ALL" entry in a devcontainer.json's "runArgs", dropping every Linux capability`,
	LongDescription: `Container runtimes grant a default set of capabilities that a dev container almost never uses: raw network
access, changing file ownership, or binding privileged ports. Dropping all of them and adding back only
what the workload needs ("capAdd") means a process that is compromised inherits no privilege the project
never asked for.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.docker.com/engine/security/#linux-kernel-capabilities`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "runArgs": ["--cap-drop=ALL"],
  "capAdd": ["CHOWN", "SETUID", "SETGID"]
}
`},
			},
		},
		Note: "`" + `runArgs` + "`" + ` is the only place this can be expressed: devcontainer.json has a
` + "`" + `capAdd` + "`" + ` property but no ` + "`" + `capDrop` + "`" + ` one, so dropping capabilities means passing
the flag to the container runtime. Add back through ` + "`" + `capAdd` + "`" + ` whatever the
workload actually needs.`,
	},
	Check: checkRequireCapDropAll,
}

RequireCapDropAll reports a devcontainer.json that does not drop all Linux capabilities via a "--cap-drop=ALL" entry in "runArgs". Dropping every capability and adding back only what's needed (e.g. via "capAdd") follows the principle of least privilege. It is off by default because most configs don't set it and enabling it by default would be noisy.

View Source
var RequireNoNewPrivileges = &linter.Rule{
	ID:          "require-no-new-privileges",
	Description: `require "no-new-privileges" to be set via a devcontainer.json's "securityOpt" property, or a "--security-opt no-new-privileges..." entry in "runArgs"`,
	LongDescription: `Without this option a process in the container can still gain privileges it was not started with, by
executing a setuid binary — which undercuts the point of running as a non-root user. Setting it raises the
kernel's "no_new_privs" bit, which every child process inherits and none can clear, so the container's
privileges can only ever shrink.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#general-properties`,
		`https://docs.kernel.org/userspace-api/no_new_privs.html`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "securityOpt": ["no-new-privileges"]
}
`},
			},
		},
	},
	Check: checkRequireNoNewPrivileges,
}

RequireNoNewPrivileges reports a devcontainer.json that does not set "no-new-privileges", either via the "securityOpt" property or a "--security-opt no-new-privileges..." entry in "runArgs". Without it, processes in the container can gain additional privileges through setuid/setgid binaries. It is off by default because most configs don't set it and enabling it by default would be noisy.

View Source
var RequireNonRoot = &linter.Rule{
	ID:          "require-non-root",
	Description: `require "remoteUser" or, if unset, "containerUser" to be set to a non-root user`,
	LongDescription: `"remoteUser" defaults to whatever user the container runs as, which for most images is root. Everything
the developer's session drives then runs as root: lifecycle scripts, terminals, and the language servers
and build tools the editor starts, so a compromised dependency runs with full control of the container.
Naming an unprivileged user — as the specification's own images do — costs nothing and contains it.`,
	References: []string{
		`https://containers.dev/implementors/json_reference/#remoteUser`,
		`https://containers.dev/implementors/spec/#users`,
	},
	Category:  linter.CategorySecurity,
	FileTypes: []linter.FileType{linter.Devcontainer},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "remoteUser": "root"
}
`},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{Path: `devcontainer.json`, Content: `{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "remoteUser": "vscode"
}
`},
			},
		},
		Note: "`" + `remoteUser` + "`" + ` is what lifecycle scripts and the editor's remote session run as,
and it wins over ` + "`" + `containerUser` + "`" + `; a rule that finds neither reports the
container's default, which is ` + "`" + `root` + "`" + ` for most images.`,
	},
	Check: checkRequireNonRoot,
}

RequireNonRoot reports a devcontainer.json that does not clearly configure a non-root user. Per the devcontainer.json spec, "remoteUser" is the user any lifecycle script and remote editor/IDE server or terminal session runs as, defaulting to "containerUser" (and, ultimately, the image's own default user) when unset. Both properties are therefore consulted: "remoteUser" first, then "containerUser" if "remoteUser" is unset. It is off by default because most configs don't set either property and enabling it by default would be noisy.

View Source
var UndefinedTemplateOption = &linter.Rule{
	ID:          "undefined-template-option",
	Description: "disallow a `${templateOption:...}` reference to an option not declared in devcontainer-template.json",
	LongDescription: `Applying a Template replaces each "${templateOption:name}" with the value the user chose for the option of
that name. A reference to an option that "options" does not declare is never prompted for, and the
reference implementation substitutes the empty string for it, so a typo silently produces an empty value
in the applied files instead of an error.`,
	References: []string{
		`https://containers.dev/implementors/templates/#options`,
	},
	Category:  linter.CategoryCorrectness,
	FileTypes: []linter.FileType{linter.Template},
	Paths:     []string{""},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{
					Path: `devcontainer-template.json`,
					Content: `{
  "id": "dotnet",
  "version": "1.0.0",
  "name": "C# (.NET)",
  "options": {
    "imageVariant": {
      "type": "string",
      "proposals": ["8.0", "9.0"],
      "default": "9.0"
    }
  }
}
`,
				},
				{
					Path: `.devcontainer/devcontainer.json`,
					Content: `{
  "image": "mcr.microsoft.com/devcontainers/dotnet:${templateOption:variant}"
}
`,
				},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{
					Path: `devcontainer-template.json`,
					Content: `{
  "id": "dotnet",
  "version": "1.0.0",
  "name": "C# (.NET)",
  "options": {
    "imageVariant": {
      "type": "string",
      "proposals": ["8.0", "9.0"],
      "default": "9.0"
    }
  }
}
`,
				},
				{
					Path: `.devcontainer/devcontainer.json`,
					Content: `{
  "image": "mcr.microsoft.com/devcontainers/dotnet:${templateOption:imageVariant}"
}
`,
				},
			},
		},
	},
	Check: checkUndefinedTemplateOption,
}

UndefinedTemplateOption reports a ${templateOption:name} reference in a Template file whose name is not declared in devcontainer-template.json's "options". The reference implementation silently substitutes such a reference with the empty string, so the misconfiguration is otherwise invisible.

View Source
var UnusedTemplateOption = &linter.Rule{
	ID:          "unused-template-option",
	Description: "disallow a Template option that no file in the Template references",
	LongDescription: `An option only takes effect where a file substitutes it as "${templateOption:name}". One that nothing
references is still presented to the user when the Template is applied, so it asks a question whose
answer changes nothing — usually a leftover from a removed file or a renamed reference.`,
	References: []string{
		`https://containers.dev/implementors/templates/#options`,
		`https://containers.dev/implementors/templates/#option-resolution`,
	},
	Category:  linter.CategoryStyle,
	FileTypes: []linter.FileType{linter.Template},
	Paths:     []string{"/options"},
	Example: linter.Example{
		Bad: linter.Snippet{
			Files: []linter.ExampleFile{
				{
					Path: `devcontainer-template.json`,
					Content: `{
  "id": "dotnet",
  "version": "1.0.0",
  "name": "C# (.NET)",
  "options": {
    "imageVariant": {
      "type": "string",
      "proposals": ["8.0", "9.0"],
      "default": "9.0"
    }
  }
}
`,
				},
				{
					Path: `.devcontainer/devcontainer.json`,
					Content: `{
  "image": "mcr.microsoft.com/devcontainers/dotnet:9.0"
}
`,
				},
			},
		},
		Good: linter.Snippet{
			Files: []linter.ExampleFile{
				{
					Path: `devcontainer-template.json`,
					Content: `{
  "id": "dotnet",
  "version": "1.0.0",
  "name": "C# (.NET)",
  "options": {
    "imageVariant": {
      "type": "string",
      "proposals": ["8.0", "9.0"],
      "default": "9.0"
    }
  }
}
`,
				},
				{
					Path: `.devcontainer/devcontainer.json`,
					Content: `{
  "image": "mcr.microsoft.com/devcontainers/dotnet:${templateOption:imageVariant}"
}
`,
				},
			},
		},
	},
	Check: checkUnusedTemplateOption,
}

UnusedTemplateOption reports a Template option declared in devcontainer-template.json's "options" that no file in the Template references as ${templateOption:name}. Such an option can never affect the applied template, so it is dead configuration.

Functions

func DocsCategoryURL added in v0.6.0

func DocsCategoryURL(name string) string

DocsCategoryURL returns the address of the rule reference's listing for the named category. The listing is an anchor on the reference's index rather than a page of its own, so the address is returned for any name, including one no category has.

func DocsURL added in v0.5.0

func DocsURL(id string) string

DocsURL returns the address of the page documenting the rule with the given ID: what it checks, why, and configuration it accepts and rejects. The address is derived from the ID, so it is returned for any id, including one no built-in rule has.

func RegisterRules

func RegisterRules(l *linter.Linter, platforms []linter.Platform, overrides Overrides) error

RegisterRules registers the built-in rules whose target platform matches platforms on l, in a deterministic order, at their default severities, unless overrides names a rule's ID or its category, in which case that severity is used instead (see Overrides.SeverityFor).

A rule is registered if it declares no target platforms (applies to all platforms), or if any of the platforms it targets is in platforms. If platforms is empty, only rules with no target platforms are registered.

RegisterRules returns an error if overrides.Rules contains a key that does not match any built-in rule ID, or if overrides.Categories contains a key that does not name a category. An override for a rule that exists but is filtered out by platforms is not an error: overriding a platform-scoped rule that hasn't been enabled is a legitimate no-op, not a typo.

Types

type Overrides added in v0.1.0

type Overrides struct {
	// Categories maps a category name (see linter.ParseCategory), matched case-insensitively, to
	// the severity every rule in that category is registered at, unless Rules overrides that rule
	// individually. A category severity also applies to rules that are off by default.
	Categories map[string]linter.Severity
	// Rules maps a rule ID to the severity that rule is registered at. It takes precedence over
	// Categories.
	Rules map[string]linter.Severity
}

Overrides carries the user-supplied severity overrides that RegisterRules applies on top of the built-in defaults.

func (Overrides) SeverityFor added in v0.1.0

func (o Overrides) SeverityFor(reg Registration) linter.Severity

SeverityFor returns the severity reg is registered at under o: the per-rule override if present, otherwise the override for the rule's category, otherwise reg's default severity.

type Registration added in v0.0.2

type Registration struct {
	// Rule is the built-in rule.
	Rule *linter.Rule
	// DefaultSeverity is the severity Rule is registered at unless overridden. It is derived from
	// Rule.Category (see categoryDefaultSeverities), not set per rule.
	DefaultSeverity linter.Severity
}

Registration pairs a built-in rule with the severity it's registered at by default.

func Builtin added in v0.0.2

func Builtin() []Registration

Builtin returns the built-in rules and their default severities, in the same deterministic order as RegisterRules uses. It is a copy of the internal registry; callers may not mutate the built-in rules through it.

func Enabled added in v0.5.0

func Enabled(platforms []linter.Platform, overrides Overrides) []Registration

Enabled returns the built-in rules RegisterRules runs for the given platforms and overrides — those it registers at a severity other than off — in the same deterministic order. Callers that report which rules a run covered, rather than which ones fired, use this.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL