diff --git a/iac/ql/lib/codeql/hcl/ast/AstNodes.qll b/iac/ql/lib/codeql/hcl/ast/AstNodes.qll index 8366c12c8e18..23ecc0374523 100644 --- a/iac/ql/lib/codeql/hcl/ast/AstNodes.qll +++ b/iac/ql/lib/codeql/hcl/ast/AstNodes.qll @@ -100,12 +100,12 @@ class HclAstNode extends THclAstNode { * } * ``` */ -class Comment extends HclAstNode, TComment { +class HCLComment extends HclAstNode, TComment { private HCL::Comment comment; - override string getAPrimaryQlClass() { result = "Comment" } + override string getAPrimaryQlClass() { result = "HCLComment" } - Comment() { this = TComment(comment) } + HCLComment() { this = TComment(comment) } /** Gets the textual contents of the comment. */ string getContents() { result = comment.getValue() } diff --git a/iac/ql/lib/codeql/iac/Comments.qll b/iac/ql/lib/codeql/iac/Comments.qll new file mode 100644 index 000000000000..815a19deeb48 --- /dev/null +++ b/iac/ql/lib/codeql/iac/Comments.qll @@ -0,0 +1,93 @@ +/** + * Provides a unified representation of comments across the infrastructure-as-code + * (IaC) languages supported by CodeQL, spanning HCL/Terraform comments and comments + * in YAML files recognized by a public IaC framework API. + */ + +import iac + +/** + * Holds if `file` is a YAML file recognized by a public IaC framework API. + */ +private predicate isSupportedYamlFile(File file) { + exists(CloudFormation::Document document | document.getFile() = file) + or + exists(ARM::Document document | document.getFile() = file) + or + exists(AzurePipelines::Document document | document.getFile() = file) + or + exists(Compose::Document document | document.getFile() = file) + or + exists(HelmChart::Document document | document.getFile() = file) + or + exists(OpenApi::Document document | document.getFile() = file) + or + exists(YamlDocument document | + document.getFile() = file and + document instanceof YamlMapping and + document + .(YamlMapping) + .lookup("$schema") + .(YamlString) + .getValue() + .regexpMatch(".*schema\\.management\\.azure\\.com.*") + ) +} + +/** + * Gets the delimiter-stripped `text` and `delimiterStyle` of an HCL comment. + */ +private predicate getHclCommentText(HCLComment comment, string text, string delimiterStyle) { + exists(string raw | raw = comment.getContents() | + raw.matches("#%") and + text = raw.suffix(1).replaceAll("\r", "") and + delimiterStyle = "hash" + or + raw.matches("//%") and + text = raw.suffix(2).replaceAll("\r", "") and + delimiterStyle = "slash" + or + raw.matches("/*%*/") and + text = raw.substring(2, raw.length() - 2).replaceAll("\r", "") and + delimiterStyle = "slash" + ) +} + +/** + * Gets the delimiter-stripped `text` and `delimiterStyle` of a comment exposed by a + * supported public IaC API located at `location`. + */ +private predicate getIacCommentText(Location location, string text, string delimiterStyle) { + exists(HCLComment comment | + location = comment.getLocation() and + getHclCommentText(comment, text, delimiterStyle) + ) + or + exists(YamlComment comment | + location = comment.getLocation() and + isSupportedYamlFile(comment.getLocation().getFile()) and + text = comment.getText() and + delimiterStyle = "hash" + ) +} + +/** + * A comment exposed by a supported public IaC API. + * + * This spans HCL/Terraform comments and comments in YAML files recognized by a public + * IaC framework API (for example CloudFormation, ARM, Azure Pipelines, Compose, Helm + * charts and OpenAPI documents). + */ +class Comment extends Location { + Comment() { + exists(string text, string delimiterStyle | getIacCommentText(this, text, delimiterStyle)) + } + + /** Gets the comment text without its delimiter. */ + string getText() { + exists(string delimiterStyle | getIacCommentText(this, result, delimiterStyle)) + } + + /** Holds if this comment uses `#` as its delimiter. */ + predicate hasHashDelimiter() { exists(string text | getIacCommentText(this, text, "hash")) } +} diff --git a/iac/ql/lib/codeql/iac/ideContextual/printAstGenerated.qll b/iac/ql/lib/codeql/iac/ideContextual/printAstGenerated.qll index bc1b0c835bf5..8671f60ffd48 100644 --- a/iac/ql/lib/codeql/iac/ideContextual/printAstGenerated.qll +++ b/iac/ql/lib/codeql/iac/ideContextual/printAstGenerated.qll @@ -21,7 +21,7 @@ class PrintAstConfiguration extends string { * Holds if the given node should be printed. */ predicate shouldPrintNode(AstNode n) { - not n instanceof Comment and + not n instanceof HCLComment and not n instanceof ReservedWord } } diff --git a/iac/ql/lib/iac.qll b/iac/ql/lib/iac.qll index d29338c0eb68..f5258b4b39dd 100644 --- a/iac/ql/lib/iac.qll +++ b/iac/ql/lib/iac.qll @@ -1,5 +1,6 @@ import codeql.Locations import codeql.files.FileSystem +import codeql.iac.Comments import codeql.iac.Dependencies // Azure import codeql.iac.azure.ARM diff --git a/iac/ql/test/library-tests/comments/Comments.expected b/iac/ql/test/library-tests/comments/Comments.expected new file mode 100644 index 000000000000..0e3b2bcd6a87 --- /dev/null +++ b/iac/ql/test/library-tests/comments/Comments.expected @@ -0,0 +1,16 @@ +iacComments +| sample.hcl:1:1:1:17 | sample.hcl@1:1:1:17 | +| sample.hcl:2:1:2:19 | sample.hcl@2:1:2:19 | +| sample.hcl:3:1:3:21 | sample.hcl@3:1:3:21 | +| sample.hcl:6:24:6:48 | sample.hcl@6:24:6:48 | +| template.yml:3:3:3:28 | template.yml@3:3:3:28 | +getText +| sample.hcl:1:1:1:17 | sample.hcl@1:1:1:17 | a hash comment | +| sample.hcl:2:1:2:19 | sample.hcl@2:1:2:19 | a slash comment | +| sample.hcl:3:1:3:21 | sample.hcl@3:1:3:21 | a block comment | +| sample.hcl:6:24:6:48 | sample.hcl@6:24:6:48 | an inline hash comment | +| template.yml:3:3:3:28 | template.yml@3:3:3:28 | a supported YAML comment | +hasHashDelimiter +| sample.hcl:1:1:1:17 | sample.hcl@1:1:1:17 | +| sample.hcl:6:24:6:48 | sample.hcl@6:24:6:48 | +| template.yml:3:3:3:28 | template.yml@3:3:3:28 | diff --git a/iac/ql/test/library-tests/comments/Comments.ql b/iac/ql/test/library-tests/comments/Comments.ql new file mode 100644 index 000000000000..699e60cf21bc --- /dev/null +++ b/iac/ql/test/library-tests/comments/Comments.ql @@ -0,0 +1,7 @@ +private import iac + +query predicate iacComments(Comment c) { any() } + +query predicate getText(Comment c, string text) { text = c.getText() } + +query predicate hasHashDelimiter(Comment c) { c.hasHashDelimiter() } diff --git a/iac/ql/test/library-tests/comments/sample.hcl b/iac/ql/test/library-tests/comments/sample.hcl new file mode 100644 index 000000000000..9f5ba943bf44 --- /dev/null +++ b/iac/ql/test/library-tests/comments/sample.hcl @@ -0,0 +1,7 @@ +# a hash comment +// a slash comment +/* a block comment */ + +resource "aws_instance" "example" { + ami = "ami-12345678" # an inline hash comment +} diff --git a/iac/ql/test/library-tests/comments/template.yml b/iac/ql/test/library-tests/comments/template.yml new file mode 100644 index 000000000000..e1a9bccb2130 --- /dev/null +++ b/iac/ql/test/library-tests/comments/template.yml @@ -0,0 +1,5 @@ +AWSTemplateFormatVersion: "2010-09-09" +Resources: + # a supported YAML comment + ExampleBucket: + Type: AWS::S3::Bucket diff --git a/iac/ql/test/library-tests/comments/unsupported.yml b/iac/ql/test/library-tests/comments/unsupported.yml new file mode 100644 index 000000000000..b1f79be61256 --- /dev/null +++ b/iac/ql/test/library-tests/comments/unsupported.yml @@ -0,0 +1,2 @@ +# an unsupported YAML comment that should be ignored +foo: bar