Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into go2rego
Browse files Browse the repository at this point in the history
  • Loading branch information
nikpivkin committed Jul 8, 2024
2 parents 68c9eff + 3c54ac8 commit 28969c1
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 78 deletions.
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The directory structure is broken down as follows:

- `cmd/` - These CLI tools are primarily used during development for end-to-end testing without needing to pull the library into trivy/tfsec etc.
- `checks` - All of the checks are defined in this directory.
- `commands` - All Node-collector commands are defined in this directory.
- `pkg/spec` - Logic to handle standardized specs such as CIS.
- `pkg/rules` - This package exposes internal rules, and imports them accordingly (see _rules.go_).
- `specs/` - Standaridized compliance specs such as CIS.
Expand Down
57 changes: 40 additions & 17 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Welcome, and thank you for considering contributing to trivy-checks!

The following guide gives an overview of the project and some directions on how to make common types of contribution. If something is missing, or you get stuck, please [start a discussion](https://github.com/aquasecurity/trivy/discussions/new) and we'll do our best to help.

### Writing Checks
## Writing Checks

Writing a new rule can be relatively simple, but there are a few things to keep in mind. The following guide will help you get started.

Expand Down Expand Up @@ -47,10 +47,10 @@ A simple rule looks like the following example:
package builtin.aws.rds.aws0176
deny[res] {
instance := input.aws.rds.instances[_]
instance.engine.value == ["postgres", "mysql"][_]
not instance.iamauthenabled.value
res := result.new("Instance does not have IAM Authentication enabled", instance.iamauthenabled)
instance := input.aws.rds.instances[_]
instance.engine.value == ["postgres", "mysql"][_]
not instance.iamauthenabled.value
res := result.new("Instance does not have IAM Authentication enabled", instance.iamauthenabled)
}
```

Expand All @@ -65,23 +65,23 @@ Let's break the metadata down.
- `scope` is used to define the scope of the policy. In this case, we are defining a policy that applies to the entire package. _defsec_ only supports using package scope for metadata at the moment, so this should always be the same.
- `schemas` tells Rego that it should use the `AWS` schema to validate the use of the input data in the policy. We currently support [these](https://github.com/aquasecurity/defsec/tree/9b3cc255faff5dc57de5ff77ed0ce0009c80a4bb/pkg/rego/schemas) schemas. Using a schema can help you validate your policy faster for syntax issues.
- `custom` is used to define custom fields that can be used by defsec to provide additional context to the policy and any related detections. This can contain the following:
- `avd_id` is the ID of the rule in the [AWS Vulnerability Database](https://avd.aquasec.com/). This is used to link the rule to the AVD entry. You can generate an ID to use for this field using `make id`.
- `provider` is the name of the provider the rule targets. This should be the same as the provider name in the `pkg/providers` directory, e.g. `aws`.
- `service` is the name of the service the rule targets. This should be the same as the service name in the `pkg/providers` directory, e.g. `rds`.
- `severity` is the severity of the rule. This should be one of `LOW`, `MEDIUM`, `HIGH`, or `CRITICAL`.
- `short_code` is a short code for the rule. This should be a short, descriptive name for the rule, separating words with hyphens. You should omit provider/service from this.
- `recommended_action` is a recommended remediation action for the rule. This should be a short, descriptive sentence describing what the user should do to resolve the issue.
- `input` tells _defsec_ what inputs this rule should be applied to. Cloud provider rules should always use the `selector` input, and should always use the `type` selector with `cloud`. Rules targeting Kubernetes yaml can use `kubenetes`, RBAC can use `rbac`, and so on.
- `subtypes` aid the engine to determine if it should load this policy or not for scanning. This can aid with the performance of scanning, especially if you have a lot of checks but not all apply to the IaC that you are trying to scan.
- `avd_id` is the ID of the rule in the [AWS Vulnerability Database](https://avd.aquasec.com/). This is used to link the rule to the AVD entry. You can generate an ID to use for this field using `make id`.
- `provider` is the name of the provider the rule targets. This should be the same as the provider name in the `pkg/providers` directory, e.g. `aws`.
- `service` is the name of the service the rule targets. This should be the same as the service name in the `pkg/providers` directory, e.g. `rds`.
- `severity` is the severity of the rule. This should be one of `LOW`, `MEDIUM`, `HIGH`, or `CRITICAL`.
- `short_code` is a short code for the rule. This should be a short, descriptive name for the rule, separating words with hyphens. You should omit provider/service from this.
- `recommended_action` is a recommended remediation action for the rule. This should be a short, descriptive sentence describing what the user should do to resolve the issue.
- `input` tells _defsec_ what inputs this rule should be applied to. Cloud provider rules should always use the `selector` input, and should always use the `type` selector with `cloud`. Rules targeting Kubernetes yaml can use `kubenetes`, RBAC can use `rbac`, and so on.
- `subtypes` aid the engine to determine if it should load this policy or not for scanning. This can aid with the performance of scanning, especially if you have a lot of checks but not all apply to the IaC that you are trying to scan.

Now you'll need to write the rule logic. This is the code that will be executed to detect the issue. You should define a rule named `deny` and place your code inside this.

```rego
deny[res] {
instance := input.aws.rds.instances[_]
instance.engine.value == ["postgres", "mysql"][_]
not instance.iamauthenabled.value
res := result.new("Instance does not have IAM Authentication enabled", instance.iamauthenabled)
instance := input.aws.rds.instances[_]
instance.engine.value == ["postgres", "mysql"][_]
not instance.iamauthenabled.value
res := result.new("Instance does not have IAM Authentication enabled", instance.iamauthenabled)
}
```

Expand All @@ -94,3 +94,26 @@ You should also write a test for your rule(s). There are many examples of these
Finally, you'll want to generate documentation for your newly added rule. Please run `make docs` to generate the documentation for your new policy and submit a PR for us to take a look at.

You can see a full example PR for a new rule being added here: [https://github.com/aquasecurity/defsec/pull/1000](https://github.com/aquasecurity/defsec/pull/1000).

## Writing Compliance reports

To write a compliance report please check the following [compliance guide](./docs/compliance.md)

Supported compliance report IDs include:

### AWS

- aws-cis-1.2
- aws-cis-1.4

### Docker

- docker-cis-1.6.0

## Kubernetes

- eks-cis-1.4
- k8s-cis-1.23
- k8s-nsa-1.0
- k8s-pss-baseline-0.1
- k8s-pss-restricted-0.1
17 changes: 17 additions & 0 deletions avd_docs/aws/s3/AVD-AWS-0089/CloudFormation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,22 @@ Resources:
LogFilePrefix: accesslogs/
```
```yaml---
Resources:
GoodExample:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub my-s3-bucket-${BucketSuffix}
LoggingConfiguration:
DestinationBucketName: !FindInMap [EnvironmentMapping, s3, logging]
LogFilePrefix: !Sub s3-logs/AWSLogs/${AWS::AccountId}/my-s3-bucket-${BucketSuffix}
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
```


14 changes: 14 additions & 0 deletions avd_docs/aws/s3/AVD-AWS-0089/Terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ resource "aws_s3_bucket" "good_example" {
}
}
```
```hcl
resource "aws_s3_bucket" "example" {
bucket = "yournamehere"
# ... other configuration ...
}
resource "aws_s3_bucket_logging" "example" {
bucket = aws_s3_bucket.example.id
target_bucket = aws_s3_bucket.log_bucket.id
target_prefix = "log/"
}
```

#### Remediation Links
Expand Down
2 changes: 1 addition & 1 deletion checks/cloud/aws/ec2/no_excessive_port_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var CheckNoExcessivePortAccess = rules.Register(
func(s *state.State) (results scan.Results) {
for _, acl := range s.AWS.EC2.NetworkACLs {
for _, rule := range acl.Rules {
if rule.Action.EqualTo("allow") && rule.Protocol.EqualTo("-1") || rule.Protocol.EqualTo("all") {
if rule.Action.EqualTo("allow") && (rule.Protocol.EqualTo("-1") || rule.Protocol.EqualTo("all")) {
results.Add(
"Network ACL rule allows access using ALL ports.",
rule.Protocol,
Expand Down
19 changes: 19 additions & 0 deletions checks/cloud/aws/ec2/no_excessive_port_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ func TestCheckNoExcessivePortAccess(t *testing.T) {
},
expected: false,
},
{
name: "Deny with protocol set to all",
input: ec2.EC2{
NetworkACLs: []ec2.NetworkACL{
{
Metadata: trivyTypes.NewTestMetadata(),
Rules: []ec2.NetworkACLRule{
{
Metadata: trivyTypes.NewTestMetadata(),
Protocol: trivyTypes.String("all", trivyTypes.NewTestMetadata()),
Type: trivyTypes.String("ingress", trivyTypes.NewTestMetadata()),
Action: trivyTypes.String("deny", trivyTypes.NewTestMetadata()),
},
},
},
},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
119 changes: 61 additions & 58 deletions cmd/avd_generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/aquasecurity/trivy/pkg/iac/framework"
_ "github.com/aquasecurity/trivy/pkg/iac/rego"
registered "github.com/aquasecurity/trivy/pkg/iac/rules"
"github.com/aquasecurity/trivy/pkg/iac/scan"
types "github.com/aquasecurity/trivy/pkg/iac/types/rules"
)

Expand All @@ -37,10 +38,12 @@ func writeDocsFile(meta types.RegisteredRule, path string) {
fail("error occurred creating the template %v\n", err)
}

rule := meta.GetRule()

docpath := filepath.Join(path,
strings.ToLower(meta.GetRule().Provider.ConstName()),
strings.ToLower(strings.ReplaceAll(meta.GetRule().Service, "-", "")),
meta.GetRule().AVDID,
strings.ToLower(rule.Provider.ConstName()),
strings.ToLower(strings.ReplaceAll(rule.Service, "-", "")),
rule.AVDID,
)

if err := os.MkdirAll(docpath, os.ModePerm); err != nil {
Expand All @@ -52,64 +55,53 @@ func writeDocsFile(meta types.RegisteredRule, path string) {
fail("error occurred creating the docs file for %s", docpath)
}

if err := tmpl.Execute(file, meta.GetRule()); err != nil {
if err := tmpl.Execute(file, rule); err != nil {
fail("error occurred generating the document %v", err)
}
fmt.Printf("Generating docs file for policy %s\n", meta.GetRule().AVDID)

if meta.GetRule().Terraform != nil {
if len(meta.GetRule().Terraform.GoodExamples) > 0 || len(meta.GetRule().Terraform.Links) > 0 {
if meta.GetRule().RegoPackage != "" { // get examples from file as rego rules don't have embedded
value, err := GetExampleValueFromFile(meta.GetRule().Terraform.GoodExamples[0], "GoodExamples")
if err != nil {
fail("error retrieving examples from metadata: %v\n", err)
}
meta.GetRule().Terraform.GoodExamples = []string{value}
}
fmt.Printf("Generating docs file for policy %s\n", rule.AVDID)

tmpl, err := template.New("terraform").Parse(terraformMarkdownTemplate)
if err != nil {
fail("error occurred creating the template %v\n", err)
}
file, err := os.Create(filepath.Join(docpath, "Terraform.md"))
if err != nil {
fail("error occurred creating the Terraform file for %s", docpath)
}
defer func() { _ = file.Close() }()
if err := generateExamplesForEngine(rule, rule.Terraform, docpath, terraformMarkdownTemplate, "Terraform"); err != nil {
fail("error generating examples for terraform: %v\n", err)
}

if err := tmpl.Execute(file, meta.GetRule()); err != nil {
fail("error occurred generating the document %v", err)
}
fmt.Printf("Generating Terraform file for policy %s\n", meta.GetRule().AVDID)
}
if err := generateExamplesForEngine(rule, rule.CloudFormation, docpath, cloudformationMarkdownTemplate, "CloudFormation"); err != nil {
fail("error generating examples for cloudformation: %v\n", err)
}
}

if meta.GetRule().CloudFormation != nil {
if len(meta.GetRule().CloudFormation.GoodExamples) > 0 || len(meta.GetRule().CloudFormation.Links) > 0 {
if meta.GetRule().RegoPackage != "" { // get examples from file as rego rules don't have embedded
value, err := GetExampleValueFromFile(meta.GetRule().CloudFormation.GoodExamples[0], "GoodExamples")
if err != nil {
fail("error retrieving examples from metadata: %v\n", err)
}
meta.GetRule().CloudFormation.GoodExamples = []string{value}
}
func generateExamplesForEngine(rule scan.Rule, engine *scan.EngineMetadata, docpath, tpl, provider string) error {
if engine == nil {
return nil
}

tmpl, err := template.New("cloudformation").Parse(cloudformationMarkdownTemplate)
if err != nil {
fail("error occurred creating the template %v\n", err)
}
file, err := os.Create(filepath.Join(docpath, "CloudFormation.md"))
if err != nil {
fail("error occurred creating the CloudFormation file for %s", docpath)
}
defer func() { _ = file.Close() }()
if len(engine.GoodExamples) == 0 {
return nil
}

if err := tmpl.Execute(file, meta.GetRule()); err != nil {
fail("error occurred generating the document %v", err)
}
fmt.Printf("Generating CloudFormation file for policy %s\n", meta.GetRule().AVDID)
if rule.RegoPackage != "" { // get examples from file as rego rules don't have embedded
examples, err := GetExampleValuesFromFile(engine.GoodExamples[0], "GoodExamples")
if err != nil {
fail("error retrieving examples from metadata: %v\n", err)
}
engine.GoodExamples = examples
}

tmpl, err := template.New(strings.ToLower(provider)).Parse(tpl)
if err != nil {
fail("error occurred creating the template %v\n", err)
}
file, err := os.Create(filepath.Join(docpath, fmt.Sprintf("%s.md", provider)))
if err != nil {
fail("error occurred creating the %s file for %s", provider, docpath)
}
defer func() { _ = file.Close() }()

if err := tmpl.Execute(file, rule); err != nil {
fail("error occurred generating the document %v", err)
}
fmt.Printf("Generating %s file for policy %s\n", provider, rule.AVDID)

return nil
}

func fail(msg string, args ...interface{}) {
Expand All @@ -123,16 +115,18 @@ func readFileFromPolicyFS(path string) (io.Reader, error) {

}

func GetExampleValueFromFile(filename string, exampleType string) (string, error) {
func GetExampleValuesFromFile(filename string, exampleType string) ([]string, error) {
r, err := readFileFromPolicyFS(filename)
if err != nil {
return "", err
return nil, err
}
f, err := parser.ParseFile(token.NewFileSet(), filename, r, parser.AllErrors)
if err != nil {
return "", err
return nil, err
}

res := []string{}

for _, d := range f.Decls {
switch decl := d.(type) {
case *goast.GenDecl:
Expand All @@ -142,17 +136,26 @@ func GetExampleValueFromFile(filename string, exampleType string) (string, error
for _, id := range spec.Names {
switch v := id.Obj.Decl.(*goast.ValueSpec).Values[0].(type) {
case *goast.CompositeLit:
value := v.Elts[0].(*goast.BasicLit).Value
if strings.Contains(id.Name, exampleType) {
return strings.ReplaceAll(value, "`", ""), nil
for _, e := range v.Elts {
switch e := e.(type) {
case *goast.BasicLit:
if strings.Contains(id.Name, exampleType) {
res = append(res, strings.ReplaceAll(e.Value, "`", ""))
}
}
}
}
}
}
}
}
}
return "", fmt.Errorf("exampleType %s not found in file: %s", exampleType, filename)

if len(res) == 0 {
return nil, fmt.Errorf("exampleType %s not found in file: %s", exampleType, filename)
}

return res, nil
}

var docsMarkdownTemplate = `
Expand Down
Loading

0 comments on commit 28969c1

Please sign in to comment.