Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IAST Security Control tests for java springboot #3576

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/weblog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,50 @@ A POST request which will receive the following JSON body:

Where the value for `value` must be used in the vulnerability.

### POST /iast/sc/*

These group of endpoints should trigger vulnerabilities detected by IAST with untrusted data coming from certain sources although the data is validated or sanitized by a configured security control

#### POST /iast/sc/s/configured

A post request using a parameter with a value that triggers a vulnerability. The value should be sanitized by a sanitizer security control configured for this vulnerability.

#### POST /sc/s/not-configured

A post request using a parameter with a value that triggers a vulnerability. The value should be sanitized by a sanitizer security control that is not configured for this vulnerability.

#### POST /sc/s/all

A post request using a parameter with a value that triggers a vulnerability. The value should be sanitized by a sanitizer security control configured for all vulnerabilities.

#### POST /sc/iv/configured

A post request using a parameter with a value that triggers a vulnerability. The value should be validated by an input validator security control configured for this vulnerability.

#### POST /sc/iv/not-configured

A post request using a parameter with a value that triggers a vulnerability. The value should be validated by an input validator security control that is not configured for this vulnerability.

#### POST /sc/iv/all

A post request using a parameter with a value that triggers a vulnerability. The value should be validated by an input validator security control configured for all vulnerabilities.

#### POST /sc/iv/overloaded/secure

A post request using two parameters that triggers a vulnerability. The values should be validated by an input validator security control with an overloaded method configured for all vulnerabilities.

#### POST /sc/iv/overloaded/insecure

A post request using two parameters that triggers a vulnerability. The values should be validated by an input validator security control with an overloaded method configured for other method signature.

#### POST /sc/s/overloaded/secure

A post request using a parameter with a value that triggers a vulnerability. The value should be sanitized by a sanitizer security control with an overloaded method configured for all vulnerabilities.

#### POST /sc/s/overloaded/insecure

A post request using a parameter with a value that triggers a vulnerability. The value should be sanitized by a sanitizer security control with an overloaded method configured for other method signature.

### GET /make_distant_call

This endpoint accept a mandatory parameter `url`. It'll make a call to these url, and should returns a JSON response :
Expand Down
1 change: 1 addition & 0 deletions manifests/cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ tests/:
test_path.py: irrelevant (ASM is not implemented in C++)
test_path_parameter.py: irrelevant (ASM is not implemented in C++)
test_uri.py: irrelevant (ASM is not implemented in C++)
test_security_controls.py: irrelevant (ASM is not implemented in C++)
rasp/:
test_lfi.py: irrelevant (ASM is not implemented in C++)
test_libddwaf.py: irrelevant (ASM is not implemented in C++)
Expand Down
2 changes: 2 additions & 0 deletions manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ tests/:
TestPathParameter: missing_feature
test_uri.py:
TestURI: irrelevant
test_security_controls.py:
TestSecurityControls: missing_feature
rasp/:
test_lfi.py:
Test_Lfi_BodyJson: v2.51.0
Expand Down
2 changes: 2 additions & 0 deletions manifests/golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ tests/:
TestPathParameter: missing_feature
test_uri.py:
TestURI: missing_feature
test_security_controls.py:
TestSecurityControls: missing_feature
rasp/:
test_lfi.py: missing_feature
test_libddwaf.py: missing_feature
Expand Down
6 changes: 6 additions & 0 deletions manifests/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,12 @@ tests/:
spring-boot-3-native: missing_feature (GraalVM. Tracing support only)
vertx3: missing_feature
vertx4: missing_feature
test_security_controls.py:
TestSecurityControls:
'*': missing_feature (No endpoint implemented)
play: missing_feature (No endpoint implemented)
ratpack: missing_feature (No endpoint implemented)
spring-boot-3-native: missing_feature (GraalVM. Tracing support only)
rasp/:
test_lfi.py:
Test_Lfi_BodyJson:
Expand Down
2 changes: 2 additions & 0 deletions manifests/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ tests/:
nextjs: missing_feature
test_uri.py:
TestURI: missing_feature
test_security_controls.py:
TestSecurityControls: missing_feature
rasp/:
test_lfi.py:
Test_Lfi_BodyJson:
Expand Down
2 changes: 2 additions & 0 deletions manifests/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ tests/:
TestPathParameter: missing_feature
test_uri.py:
TestURI: missing_feature
test_security_controls.py:
TestSecurityControls: missing_feature
rasp/:
test_lfi.py: missing_feature
test_libddwaf.py: missing_feature
Expand Down
2 changes: 2 additions & 0 deletions manifests/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ tests/:
uwsgi-poc: v2.13.0
test_uri.py:
TestURI: missing_feature
test_security_controls.py:
TestSecurityControls: missing_feature
rasp/:
test_lfi.py:
Test_Lfi_BodyJson: v2.10.0
Expand Down
2 changes: 2 additions & 0 deletions manifests/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ tests/:
TestPathParameter: missing_feature
test_uri.py:
TestURI: missing_feature
test_security_controls.py:
TestSecurityControls: missing_feature
rasp/:
test_lfi.py: missing_feature
test_libddwaf.py: missing_feature
Expand Down
133 changes: 133 additions & 0 deletions tests/appsec/iast/test_security_controls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Unless explicitly stated otherwise all files in this repository are licensed under the the Apache License Version 2.0.
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2021 Datadog, Inc.

from utils import features, rfc, weblog, interfaces
from tests.appsec.iast.utils import BaseSinkTest, assert_iast_vulnerability


@features.iast_security_controls
@rfc("https://docs.google.com/document/d/1j1hp87-2wJnXUGADZxzLnvKJmaF_Gd6ZR1hPS3LVguQ/edit?pli=1&tab=t.0")
class TestSecurityControls:
@staticmethod
def assert_iast_is_enabled(request):
product_enabled = False
for _, _, span in interfaces.library.get_spans(request=request):
# Check if the product is enabled in meta
meta = span["meta"]
if "_dd.iast.json" in meta:
product_enabled = True
break
# Check if the product is enabled in meta_struct
meta_struct = span["meta_struct"]
if meta_struct and meta_struct.get("vulnerability"):
product_enabled = True
break
assert product_enabled, "IAST is not available"

def setup_iast_is_enabled(self):
self.check_r = weblog.post("/iast/sc/iv/not-configured", data={"param": "param"})

def setup_vulnerability_suppression_with_an_input_validator_configured_for_a_specific_vulnerability(self):
self.setup_iast_is_enabled()
self.r = weblog.post("/iast/sc/iv/configured", data={"param": "param"})

def test_vulnerability_suppression_with_an_input_validator_configured_for_a_specific_vulnerability(self):
self.assert_iast_is_enabled(self.check_r)
BaseSinkTest.assert_no_iast_event(self.r, "COMMAND_INJECTION")

def setup_no_vulnerability_suppression_with_an_input_validator_configured_for_a_different_vulnerability(self):
self.setup_iast_is_enabled()
self.r = weblog.post("/iast/sc/iv/not-configured", data={"param": "param"})

def test_no_vulnerability_suppression_with_an_input_validator_configured_for_a_different_vulnerability(self):
self.assert_iast_is_enabled(self.check_r)
assert_iast_vulnerability(
request=self.r, vulnerability_count=1, vulnerability_type="SQL_INJECTION",
)

def setup_vulnerability_suppression_with_an_input_validator_configured_for_all_vulnerabilities(self):
self.setup_iast_is_enabled()
self.r = weblog.post("/iast/sc/iv/all", data={"param": "param"})

def test_vulnerability_suppression_with_an_input_validator_configured_for_all_vulnerabilities(self):
self.assert_iast_is_enabled(self.check_r)
BaseSinkTest.assert_no_iast_event(self.r, "SQL_INJECTION")

def setup_vulnerability_suppression_with_an_input_validator_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.setup_iast_is_enabled()
self.r = weblog.post("iast/sc/iv/overloaded/secure", data={"user": "usr1", "password": "pass"})

def test_vulnerability_suppression_with_an_input_validator_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.assert_iast_is_enabled(self.check_r)
BaseSinkTest.assert_no_iast_event(self.r, "SQL_INJECTION")

def setup_no_vulnerability_suppression_with_an_input_validator_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.setup_iast_is_enabled()
self.r = weblog.post("iast/sc/iv/overloaded/insecure", data={"user": "usr1", "password": "pass"})

def test_no_vulnerability_suppression_with_an_input_validator_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.assert_iast_is_enabled(self.check_r)
assert_iast_vulnerability(
request=self.r, vulnerability_count=1, vulnerability_type="SQL_INJECTION",
)

def setup_vulnerability_suppression_with_a_sanitizer_configured_for_a_specific_vulnerability(self):
self.setup_iast_is_enabled()
self.r = weblog.post("/iast/sc/s/configured", data={"param": "param"})

def test_vulnerability_suppression_with_a_sanitizer_configured_for_a_specific_vulnerability(self):
self.assert_iast_is_enabled(self.check_r)
BaseSinkTest.assert_no_iast_event(self.r, "COMMAND_INJECTION")

def setup_no_vulnerability_suppression_with_a_sanitizer_configured_for_a_different_vulnerability(self):
self.setup_iast_is_enabled()
self.r = weblog.post("/iast/sc/s/not-configured", data={"param": "param"})

def test_no_vulnerability_suppression_with_a_sanitizer_configured_for_a_different_vulnerability(self):
self.assert_iast_is_enabled(self.check_r)
assert_iast_vulnerability(
request=self.r, vulnerability_count=1, vulnerability_type="SQL_INJECTION",
)

def setup_vulnerability_suppression_with_a_sanitizer_configured_for_all_vulnerabilities(self):
self.setup_iast_is_enabled()
self.r = weblog.post("/iast/sc/s/all", data={"param": "param"})

def test_vulnerability_suppression_with_a_sanitizer_configured_for_all_vulnerabilities(self):
self.assert_iast_is_enabled(self.check_r)
BaseSinkTest.assert_no_iast_event(self.r, "SQL_INJECTION")

def setup_vulnerability_suppression_with_a_sanitizer_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.setup_iast_is_enabled()
self.r = weblog.post("iast/sc/s/overloaded/secure", data={"param": "param"})

def test_vulnerability_suppression_with_a_sanitizer_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.assert_iast_is_enabled(self.check_r)
BaseSinkTest.assert_no_iast_event(self.r, "COMMAND_INJECTION")

def setup_no_vulnerability_suppression_with_a_sanitizer_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.setup_iast_is_enabled()
self.r = weblog.post("iast/sc/s/overloaded/insecure", data={"param": "param"})

def test_no_vulnerability_suppression_with_a_sanitizer_configured_for_an_overloaded_method_with_specific_signature(
self,
):
self.assert_iast_is_enabled(self.check_r)
assert_iast_vulnerability(
request=self.r, vulnerability_count=1, vulnerability_type="COMMAND_INJECTION",
)
21 changes: 21 additions & 0 deletions utils/_context/_scenarios/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
from .endtoend import EndToEndScenario


# When Security Controls configuration is set, tracers must instrument all the designated methods in the configuration as security controls.
# RFC(https://docs.google.com/document/d/1j1hp87-2wJnXUGADZxzLnvKJmaF_Gd6ZR1hPS3LVguQ/edit?pli=1&tab=t.0)
_iast_security_controls_map = {
"cpp": "TODO",
"dotnet": "TODO",
"golang": "TODO",
"java": "SANITIZER:COMMAND_INJECTION:com.datadoghq.system_tests.iast.utils.SecurityControlUtil:sanitize;SANITIZER:*:com.datadoghq.system_tests.iast.utils.SecurityControlUtil:sanitizeForAllVulns;SANITIZER:*:com.datadoghq.system_tests.iast.utils.SecurityControlUtil:overloadedSanitize:java.lang.String;INPUT_VALIDATOR:COMMAND_INJECTION:com.datadoghq.system_tests.iast.utils.SecurityControlUtil:validate;INPUT_VALIDATOR:*:com.datadoghq.system_tests.iast.utils.SecurityControlUtil:validateForAllVulns;INPUT_VALIDATOR:*:com.datadoghq.system_tests.iast.utils.SecurityControlUtil:overloadedValidation:java.lang.Object,java.lang.String,java.lang.String:1,2",
"nodejs": "TODO",
"php": "TODO",
"python": "TODO",
"ruby": "TODO",
}


class DefaultScenario(EndToEndScenario):
def __init__(self, name: str):
super().__init__(
Expand All @@ -18,3 +32,10 @@ def __init__(self, name: str):
scenario_groups=[ScenarioGroup.ESSENTIALS],
doc="Default scenario, spawn tracer, the Postgres databases and agent, and run most of exisiting tests",
)

def configure(self, config):
super().configure(config)

library = self.weblog_container.image.env["SYSTEM_TESTS_LIBRARY"]
value = _iast_security_controls_map[library]
self.weblog_container.environment["DD_IAST_SECURITY_CONTROLS_CONFIGURATION"] = value
10 changes: 10 additions & 0 deletions utils/_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -2487,3 +2487,13 @@ def iast_stack_trace(test_object):
"""
pytest.mark.features(feature_id=329)(test_object)
return test_object

@staticmethod
def iast_security_controls(test_object):
"""
IAST: Security Controls

https://feature-parity.us1.prod.dog/#/?feature=343
"""
pytest.mark.features(feature_id=343)(test_object)
return test_object
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,93 @@ object IastRoutes {
}
}
}
pathPrefix("sc") {
pathPrefix("s") {
post {
path("configured") {
formField("param") { param =>
val sanitized = SecurityControlUtil.sanitize(param)
cmd.insecureCmd(sanitized)
complete(StatusCodes.OK)
}
} ~
path("not-configured") {
formField("param") { param =>
val sanitized = SecurityControlUtil.sanitize(param)
complete(StatusCodes.OK, sql.insecureSql(sanitized, "password"))(jsonMarshaller)
}
} ~
path("all") {
formField("param") { param =>
val sanitized = SecurityControlUtil.sanitizeForAllVulns(param)
complete(StatusCodes.OK, sql.insecureSql(sanitized, "password"))(jsonMarshaller)
}
} ~
pathPrefix("overloaded") {
path("secure") {
formField("param") { param =>
val sanitized = SecurityControlUtil.overloadedSanitize(param)
cmd.insecureCmd(sanitized)
complete(StatusCodes.OK)
}
} ~
path("insecure") {
formField("param") { param =>
val sanitized = SecurityControlUtil.overloadedSanitize(param, null)
cmd.insecureCmd(sanitized)
complete(StatusCodes.OK)
}
}
}
}
} ~
pathPrefix("iv") {
post {
path("configured") {
formField("param") { param =>
if (SecurityControlUtil.validate(param)) {
cmd.insecureCmd(param)
}
complete(StatusCodes.OK)
}
} ~
path("not-configured") {
formField("param") { param =>
if (SecurityControlUtil.validate(param)) {
sql.insecureSql(param, "password")
}
complete(StatusCodes.OK)
}
} ~
path("all") {
formField("param") { param =>
if (SecurityControlUtil.validateForAllVulns(param)) {
sql.insecureSql(param, "password")
}
complete(StatusCodes.OK)
}
} ~
pathPrefix("overloaded") {
path("secure") {
formFields("user", "password") { (user, pass) =>
if (SecurityControlUtil.overloadedValidation(null, user, pass)) {
sql.insecureSql(user, pass)
}
complete(StatusCodes.OK)
}
} ~
path("insecure") {
formFields("user", "password") { (user, pass) =>
if (SecurityControlUtil.overloadedValidation(user, pass)) {
sql.insecureSql(user, pass)
}
complete(StatusCodes.OK)
}
}
}
}
}
}
}

private def paramOrFormField(p: String) = {
Expand Down
Loading
Loading