Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/execute/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Build images used for system tests.
+ Specific to the `GRAPHQL_APPSEC` scenario: `gqlgen`, `graph-gophers`, `graphql-go`
* For `java`: `spring-boot` (default),`akka-http`,`jersey-grizzly2`,`play`,`ratpack`,`resteasy-netty3`,`spring-boot-3-native`,`spring-boot-jetty`,`spring-boot-openliberty`,`spring-boot-payara`,`spring-boot-undertow`,`spring-boot-wildfly`,`uds-spring-boot`,`vertx3`,`vertx4`
* For `nodejs`: `express4` (default), `express4-typescript`, `express5`, `nextjs`, `fastify`
* For `php`: `apache-mod-8.0` (default), `apache-mod-8.1`, `apache-mod-8.2`, `apache-mod-7.4`, `apache-mod-7.3`, `apache-mod-7.2`, `apache-mod-7.1`, `apache-mod-7.0`, `apache-mod-8.2-zts`, `apache-mod-8.1-zts`, `apache-mod-8.0-zts`, `apache-mod-7.4-zts`, `apache-mod-7.3-zts`, `apache-mod-7.2-zts`, `apache-mod-7.1-zts`, `apache-mod-7.0-zts`, `php-fpm-8.5`, `php-fpm-8.2`, `php-fpm-8.1`, `php-fpm-8.0`, `php-fpm-7.4`, `php-fpm-7.3`, `php-fpm-7.2`, `php-fpm-7.1`, `php-fpm-7.0`, `laravel11x`
* For `php`: `apache-mod-8.0` (default), `apache-mod-8.1`, `apache-mod-8.2`, `apache-mod-7.4`, `apache-mod-7.3`, `apache-mod-7.2`, `apache-mod-7.1`, `apache-mod-7.0`, `apache-mod-8.2-zts`, `apache-mod-8.1-zts`, `apache-mod-8.0-zts`, `apache-mod-7.4-zts`, `apache-mod-7.3-zts`, `apache-mod-7.2-zts`, `apache-mod-7.1-zts`, `apache-mod-7.0-zts`, `php-fpm-8.5`, `php-fpm-8.2`, `php-fpm-8.1`, `php-fpm-8.0`, `php-fpm-7.4`, `php-fpm-7.3`, `php-fpm-7.2`, `php-fpm-7.1`, `php-fpm-7.0`, `laravel11x`, `symfony7x`
* For `python`: `flask-poc` (default), `fastapi`, `uwsgi-poc`, `django-poc`, `python3.12`
* For `ruby`: `rails70` (default), `rack`, `sinatra21`, and lot of other sinatra/rails versions

Expand Down
43 changes: 42 additions & 1 deletion manifests/php.yml

Large diffs are not rendered by default.

34 changes: 21 additions & 13 deletions tests/appsec/test_automated_login_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,10 +1155,13 @@ def validate_iden(meta: dict):
libs_without_user_id_on_failure = ["nodejs", "java"]

# Weblog variants that omit appsec.events.users.login.failure.usr.exists (library-wide skips use libs_without_user_exist).
weblogs_without_user_exist = ["laravel11x"]
weblogs_without_user_exist = ["laravel11x", "symfony7x"]

# Weblog variants that omit usr.id on login failure (library-wide: libs_without_user_id_on_failure).
weblogs_without_user_id_on_failure = ["laravel11x"]
weblogs_without_user_id_on_failure = ["laravel11x", "symfony7x"]

# Weblog variants that omit usr.id on login success (library-wide: libs_without_user_id).
weblogs_without_user_id = ["symfony7x"]


def login_failure_includes_usr_exists_meta() -> bool:
Expand All @@ -1174,6 +1177,11 @@ def login_failure_includes_usr_id_meta() -> bool:
)


def login_success_includes_usr_id_meta() -> bool:
"""True when this library and weblog emit usr.id on login success spans."""
return context.library not in libs_without_user_id and context.weblog_variant not in weblogs_without_user_id


@rfc("https://docs.google.com/document/d/1RT38U6dTTcB-8muiYV4-aVDCsT_XrliyakjtAPyjUpw")
@features.user_monitoring
@features.user_id_collection_modes
Expand Down Expand Up @@ -1226,7 +1234,7 @@ def test_login_success_local(self):
assert_boolean_meta_tag(meta, "appsec.events.users.login.success.track")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "social-security-id"
assert meta["_dd.appsec.usr.id"] == "social-security-id"

Expand All @@ -1248,7 +1256,7 @@ def test_login_success_basic(self):
assert_boolean_meta_tag(meta, "appsec.events.users.login.success.track")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "social-security-id"
assert meta["_dd.appsec.usr.id"] == "social-security-id"

Expand Down Expand Up @@ -1364,7 +1372,7 @@ def test_login_sdk_success_local(self):
assert_boolean_meta_tag(meta, "_dd.appsec.events.users.login.success.sdk")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == "social-security-id"

Expand Down Expand Up @@ -1394,7 +1402,7 @@ def test_login_sdk_success_basic(self):
assert_boolean_meta_tag(meta, "_dd.appsec.events.users.login.success.sdk")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == "social-security-id"

Expand Down Expand Up @@ -1462,7 +1470,7 @@ def test_signup_local(self):
assert_boolean_meta_tag(meta, "appsec.events.users.signup.track")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["appsec.events.users.signup.usr.id"] == "new-user"
assert meta["_dd.appsec.usr.id"] == "new-user"

Expand Down Expand Up @@ -1532,7 +1540,7 @@ def test_login_success_local(self):
assert is_same_boolean(actual=meta["appsec.events.users.login.success.track"], expected="true")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == USER_HASH
assert meta["_dd.appsec.usr.id"] == USER_HASH

Expand All @@ -1552,7 +1560,7 @@ def test_login_success_basic(self):
assert is_same_boolean(actual=meta["appsec.events.users.login.success.track"], expected="true")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == USER_HASH
assert meta["_dd.appsec.usr.id"] == USER_HASH

Expand Down Expand Up @@ -1668,7 +1676,7 @@ def test_login_sdk_success_local(self):
assert is_same_boolean(actual=meta["_dd.appsec.events.users.login.success.sdk"], expected="true")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == USER_HASH

Expand Down Expand Up @@ -1696,7 +1704,7 @@ def test_login_sdk_success_basic(self):
assert is_same_boolean(actual=meta["_dd.appsec.events.users.login.success.sdk"], expected="true")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == USER_HASH

Expand Down Expand Up @@ -1764,7 +1772,7 @@ def test_signup_local(self):
assert is_same_boolean(actual=meta["appsec.events.users.signup.track"], expected="true")

# optional (to review for each library)
if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():
assert meta["appsec.events.users.signup.usr.id"] == NEW_USER_HASH
assert meta["_dd.appsec.usr.id"] == NEW_USER_HASH

Expand Down Expand Up @@ -1903,7 +1911,7 @@ def test_login_event_blocking_auto_id(self):
assert self.config_state_1.state == rc.ApplyState.ACKNOWLEDGED
assert self.config_state_2.state == rc.ApplyState.ACKNOWLEDGED

if context.library not in libs_without_user_id:
if login_success_includes_usr_id_meta():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the blocking test from passing without assertions

For WEBLOG_VARIANT=symfony7x, the new helper returns false, so test_login_event_blocking_auto_id only checks the initial login and remote-config ACKs and never validates that self.r_login_blocked produced the block-user-id attack or a 403. That turns an enabled test into a no-op for this weblog; mark the Symfony case in the manifest or split the expectation instead of gating the core assertions at runtime.

Useful? React with 👍 / 👎.

interfaces.library.assert_waf_attack(self.r_login_blocked, rule="block-user-id")
assert self.r_login_blocked.status_code == 403

Expand Down
60 changes: 60 additions & 0 deletions utils/build/docker/php/symfony7x.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
ARG PHP_VERSION=8.2
ARG VARIANT=release

FROM datadog/dd-appsec-php-ci:php-$PHP_VERSION-$VARIANT

ENV PHP_VERSION=8.2
ENV DD_TRACE_ENABLED=1
ENV DD_TRACE_GENERATE_ROOT_SPAN=1
ENV DD_TRACE_AGENT_FLUSH_AFTER_N_REQUESTS=0
ENV DD_TRACE_HEADER_TAGS=user-agent

EXPOSE 7777/tcp

ADD binaries* /binaries/
ADD utils/build/docker/php /tmp/php

# Pre-create .env with APP_SECRET and APP_ENV
RUN mkdir -p /var/www/html && \
echo "APP_ENV=prod" > /var/www/html/.env && \
php -r "echo 'APP_SECRET=' . bin2hex(random_bytes(16)) . PHP_EOL;" >> /var/www/html/.env && \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load or export the Symfony secret

This only writes APP_SECRET into .env, but neither bin/console nor public/index.php loads Dotenv, and symfony/dotenv is not required by the new app. Because config/packages/framework.yaml reads %env(APP_SECRET)%, the Apache runtime environment will not contain the secret that Symfony's security/session services need, so login/session-backed requests can fail even though the file exists. Export the value into the container environment or explicitly load Dotenv before booting the kernel.

Useful? React with 👍 / 👎.

echo "APP_DEBUG=0" >> /var/www/html/.env && \
echo "SYMFONY_DB_PATH=/tmp/symfony.db" >> /var/www/html/.env

RUN chmod +x /tmp/php/apache-mod/build.sh
RUN /tmp/php/apache-mod/build.sh symfony7x

# Use Symfony-specific Apache config (DocumentRoot public/, AllowOverride All)
RUN cp /tmp/php/weblogs/symfony7x/.apache.conf /etc/apache2/mods-available/php.conf && \
sed -i "s/%PHP_MAJOR_VERSION//g" /etc/apache2/mods-available/php.conf && \
ln -sf /etc/apache2/mods-available/php.conf /etc/apache2/mods-enabled/php.conf

# pdo_sqlite, dom, and tokenizer are needed by Symfony (mirrors laravel11x pattern; tokenizer for attribute routing)
RUN printf "extension=dom.so\nextension=pdo_sqlite.so\nextension=tokenizer.so\n" >> /etc/php/php.ini

# Create log directory before running PHP (php.ini writes error_log there)
RUN mkdir -p /var/log/system-tests

# Create SQLite database and seed test users (script is at /var/www/html/bin/init-db.php via build.sh)
RUN SYMFONY_DB_PATH=/tmp/symfony.db php /var/www/html/bin/init-db.php && \
chmod 666 /tmp/symfony.db

# Warm up Symfony cache
RUN cd /var/www/html && APP_ENV=prod php bin/console cache:warmup

# Install ddtrace (same pattern as apache-mod-X.Y.Dockerfiles)
ADD utils/build/docker/php/common/install_ddtrace.sh /install_ddtrace.sh
RUN /install_ddtrace.sh 1

# Set writable permissions on var/ directory
RUN chmod -R 775 /var/www/html/var && \
chown -R www-data:www-data /var/www/html/var

RUN rm -rf /tmp/php/

ADD utils/build/docker/php/apache-mod/entrypoint.sh /
WORKDIR /binaries
ENTRYPOINT []
RUN echo "#!/bin/bash\ndumb-init /entrypoint.sh" > app.sh
RUN chmod +x app.sh
CMD [ "./app.sh" ]
15 changes: 15 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/.apache.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<IfModule mod_php%PHP_MAJOR_VERSION.c>
<VirtualHost *:7777>
DocumentRoot /var/www/html/public
AllowEncodedSlashes NoDecode
<Directory /var/www/html/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
<FilesMatch "\.php$">
SetHandler application/x-httpd-php
Require all granted
</FilesMatch>
PHPIniDir "/etc/php/php.ini"
</IfModule>
12 changes: 12 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/Acme/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "weblog/acme",
"description": "Just a package to test the composer libraries are sent",
"type": "library",
"require": {},
"version": "1.0.0",
"autoload": {
"psr-4": {
"Acme\\": "src/"
}
}
}
7 changes: 7 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/Acme/src/Acme.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Acme;

class Acme {

}
16 changes: 16 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env php
<?php

use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;

require dirname(__DIR__).'/vendor/autoload.php';

$input = new ArgvInput();
$env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'prod', true);
$debug = (bool) ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption('--no-debug', true);

$kernel = new Kernel($env, $debug);
$app = new Application($kernel);
$app->run($input);
37 changes: 37 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/bin/init-db.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env php
<?php

use App\Entity\User;
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\SchemaTool;

require dirname(__DIR__) . '/vendor/autoload.php';

$dbPath = getenv('SYMFONY_DB_PATH') ?: '/tmp/symfony.db';

$config = ORMSetup::createAttributeMetadataConfiguration(
paths: [dirname(__DIR__) . '/src/Entity'],
isDevMode: false,
);

$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => $dbPath]);
$em = new EntityManager($connection, $config);

(new SchemaTool($em))->createSchema($em->getMetadataFactory()->getAllMetadata());

$seeds = [
['id' => 'social-security-id', 'username' => 'test', 'password' => '1234'],
['id' => '591dc126-8431-4d0f-9509-b23318d3dce4', 'username' => 'testuuid', 'password' => '1234'],
];

foreach ($seeds as $seed) {
if ($em->find(User::class, $seed['id']) === null) {
$em->persist(new User($seed['id'], $seed['username'], password_hash($seed['password'], PASSWORD_BCRYPT), $seed['username']));
}
}

$em->flush();

echo "Database initialized at $dbPath\n";
59 changes: 59 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "php/symfony-weblog",
"type": "project",
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/orm": "^3.3",
"stripe/stripe-php": "^10.0",
"symfony/console": "^7.2",
"symfony/framework-bundle": "^7.2",
"symfony/security-bundle": "^7.2",
"symfony/dom-crawler": "^7.2",
"symfony/filesystem": "^7.2",
"symfony/http-client": "^7.2",
"symfony/process": "^7.2",
"symfony/yaml": "^7.2",
"monolog/monolog": "^3.0",
"weblog/acme": "*"
},
"repositories": [
{
"type": "path",
"url": "./Acme",
"options": {
"symlink": true
}
}
],
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"symfony/flex": false
},
"platform": {
"ext-ctype": "8.2.0",
"ext-iconv": "8.2.0",
"ext-curl": "8.2.0",
"ext-json": "8.2.0",
"ext-mbstring": "8.2.0",
"ext-openssl": "8.2.0",
"ext-pdo": "8.2.0",
"ext-session": "8.2.0",
"ext-tokenizer": "8.2.0",
"ext-dom": "8.2.0",
"ext-xml": "8.2.0"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
7 changes: 7 additions & 0 deletions utils/build/docker/php/weblogs/symfony7x/config/bundles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

return [
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
parameters:
default_sqlite_path: '/tmp/symfony.db'

doctrine:
dbal:
default_connection: default
connections:
default:
driver: pdo_sqlite
path: '%env(default:default_sqlite_path:SYMFONY_DB_PATH)%'
server_version: '3.39'
mysql:
driver: pdo_mysql
host: mysqldb
dbname: mysql_dbname
user: mysqldb
password: mysqldb
server_version: '8.0'
postgres:
driver: pdo_pgsql
host: postgres
port: 5433
dbname: system_tests_dbname
user: system_tests_user
password: system_tests
server_version: '14.0'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
App:
is_bundle: false
type: attribute
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
Loading
Loading