Welcome, and thank you for considering contributing to defsec!
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 jump on Slack or start a discussion and we'll do our best to help.
defsec is a library for defining security rules and policies in code, and the tools to apply those rules/cloud/policies to a variety of sources. The general architecture and project layout are defined in ARCHITECTURE.md - this is a great place to start exploring.
defsec is also the misconfiguration/IaC/Cloud scanning engine for Trivy. Trivy uses defsec internally as a library to perform various scans.
The following are guides for contributing to the project in specific ways. If you're not sure where to start, these are a good place to look. If you need some tips on getting started with contributing to open source in general, check out this useful GitHub contribution guide.
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.
First of all, you should check if the provider your rule targets is supported by defsec. If it's not, you'll need to add support for it. See Adding Support for a New Cloud Provider for more information. You can check if support exists by looking for a directory with the provider name in pkg/providers
. If you find your provider, navigate into the directory and check for a directory with the name of the service you're targeting. If you can't find that, you'll need to add support for it. See Adding Support for a New Service for more information.
Next up, you'll need to check if the properties you want to target are supported, and if not, add support for them. The guide on Adding Support for a New Service covers adding new properties.
At last, it's time to write your rule code! Rules are defined using OPA Rego. You can find a number of examples in the rules/cloud/policies
directory. The OPA documentation is a great place to start learning Rego. You can also check out the Rego Playground to experiment with Rego, and join the OPA Slack.
Create a new file in rules/cloud/policies
with the name of your rule. You should nest it in the existing directory structure as applicable. The package name should be in the format builtin.PROVIDER.SERVICE.ID
, e.g. builtin.aws.rds.aws0176
.
Running make id
will provide you with the next available ID for your rule. You can use this ID in your rule code to identify it.
A simple rule looks like the following example:
# METADATA
# title: "RDS IAM Database Authentication Disabled"
# description: "Ensure IAM Database Authentication is enabled for RDS database instances to manage database access"
# scope: package
# schemas:
# - input: schema.input
# related_resources:
# - https://docs.aws.amazon.com/neptune/latest/userguide/iam-auth.html
# custom:
# avd_id: AVD-AWS-0176
# provider: aws
# service: rds
# severity: MEDIUM
# short_code: enable-iam-auth
# recommended_action: "Modify the PostgreSQL and MySQL type RDS instances to enable IAM database authentication."
# input:
# selector:
# - type: cloud
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)
}
In fact, this is the code for an actual rule. You can find it in rules/cloud/policies/aws/rds/enable_iam_auth.rego
.
The metadata is the top section that starts with # METADATA
, and is fairly verbose. You can copy and paste from another rule as a starting point. This format is effectively yaml within a Rego comment, and is defined as part of Rego itself.
Let's break the metadata down.
title
is fairly self explanatory - it is a title for the rule. The title should clearly and succinctly state the problem which is being detected.description
is also fairly self explanatory - it is a description of the problem which is being detected. The description should be a little more verbose than the title, and should describe what the rule is trying to achieve. Imagine it completing a sentence starting withYou should...
.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 theschema.input
to validate the use of the input data in the policy. Generally you can use this as-is in order to detect errors in your policy, such as referencing a policy which doesn't exist in the defsec schema.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. This is used to link the rule to the AVD entry. You can generate an ID to use for this field usingmake id
.provider
is the name of the provider the rule targets. This should be the same as the provider name in thepkg/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 thepkg/providers
directory, e.g.rds
.severity
is the severity of the rule. This should be one ofLOW
,MEDIUM
,HIGH
, orCRITICAL
.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 theselector
input, and should always use thetype
selector withcloud
. Rules targeting Kubernetes yaml can usekubenetes
, RBAC can userbac
, and so on.
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.
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)
}
The rule should return a result, which can be created using result.new
(this function does not need to be imported, it is defined internally and provided at runtime). The first argument is the message to display, and the second argument is the resource that the issue was detected on.
In the example above, you'll notice properties are being accessed from the input.aws
object. The full set of schemas containing all of these properties is available here. You can match the schema name to the type of input you want to scan.
You should also write a test for your rule(s). There are many examples of these in the rules/cloud/policies
directory.
Finally, you'll want to run make docs
to generate the documentation for your new policy.
You can see a full example PR for a new rule being added here: aquasecurity#1000.
If you want to add support for a new cloud provider, you'll need to add a new subdirectory to the pkg/providers
directory, named after your provider. Inside this, create a Go file with the same name, and create a struct to hold information about all of the services supported by your provider.
For example, adding support for a new provider called foo
would look like this:
pkg/providers/foo/foo.go
:
package foo
type Foo struct {
// Add services here later...
}
Next you should add a reference to your provider struct in pkg/state/state.go
:
type State struct {
// ...
Foo foo.Foo
// ...
}
Next up you'll need to add one or more adapters to internal/adapters
. An adapter takes an input and populates your provider struct. For example, if you want to scan a Terraform plan, you'll need to add an adapter that takes the Terraform plan and populates your provider struct. The AWS provider support in defsec uses multiple adapters - it can adapt CloudFormation, Terraform, and live AWS accounts. Each of these has an adapter in this directory.
To support Terraform as an input, your adapter should look something like this:
func Adapt(modules terraform.Modules) (foo.Foo, error) {
return foo.Foo{
// ...
}, nil
}
...and should be called in internal/adapters/terraform/adapt.go
.
It's a good idea to browse the existing adapters to see how they work, as there is a lot of common code that can be reused.
Adding a new service involves two steps. The service will need a data structure to store information about the required resources, and then one or more adapters to convert input(s) into the aforementioned data structure.
To add a new service named bar
to a provider named foo
, you'll need to add a new file at pkg/providers/foo/bar/bar.go
:
type Bar struct {
// ...
}
Let's say the Bar
service manages resources called Baz
. You'll need to add a new struct to the Bar
struct to hold information about this resource:
type Bar struct {
// ...
Baz []Baz
// ...
}
type Baz struct {
types.Metadata
Name types.StringValue
Encrypted types.BoolValue
}
A Baz can have a name, and can optionally be encrypted. Instead of using raw string
and bool
types respectively, we use the defsec types types.StringValue
and types.BoolValue
. These types wrap the raw values and provide additional metadata about the value, such as whether it was set by the user or not, and the file and line number where the resource was defined. The types.Metadata
struct is embedded in all of the defsec types, and provides a common set of metadata for all resources. This includes the file and line number where the resource was defined, and the name of the resource.
Next you'll need to add a reference to your new service struct in the provider struct at pkg/providers/foo/foo.go
:
type Foo struct {
// ...
Bar bar.Bar
// ...
}
Now you'll need to update all of the adapters which populate the Foo
provider struct. For example, if you want to support Terraform, you'll need to update internal/adapters/terraform/foo/bar/adapt.go
.
Finally, make sure you run make schema
to generate the schema for your new service.