October 20, 2026
Penetration Testing With AWS Security Agent
Reference: https://aws.amazon.com/blogs/security/penetration-testing-every-deployment-with-aws-continuum/

By Vinayak Pandey
4 min read
Introduction Learn what AWS Security Agent (now part of AWS Continuum) does, how it works, and how its resources fit together.
https://docs.aws.amazon.com/securityagent/latest/userguide/cicd-pentest.html
Step 1: Create an EC2 instance with Amazon Linux 2023 AMI and connect to it and run following commands as ec2-user
sudo dnf install -y git
sudo dnf groupinstall "Development Tools" -y
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 24
npm install -g pm2sudo dnf install -y git
sudo dnf groupinstall "Development Tools" -y
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 24
npm install -g pm2Step 2: Clone https://github.com/juice-shop/juice-shop.git and push the code to a private GitLab repo using following commands
git remote rename origin old-origin
git remote add origin https://gitlab.com/<GITLAB_REPO_URL>
git push --set-upstream origin --all
git push --set-upstream origin --tagsgit remote rename origin old-origin
git remote add origin https://gitlab.com/<GITLAB_REPO_URL>
git push --set-upstream origin --all
git push --set-upstream origin --tagsGo to Settings โ Repository โ Deploy tokens and create one with the read_repository scope. Set following CI/CD variables.
and add .gitlab-ci.yml
stages: [deploy]
deploy_ec2:
stage: deploy
image: alpine:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
before_script:
- apk add --no-cache openssh-client
- chmod 600 "$SSH_PRIVATE_KEY"
script:
- |
ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=accept-new \
"$EC2_USER@$EC2_HOST" \
"REPO_URL='@gitlab.com/cloudfrontvinayak/juice-shop.git'">https://${DEPLOY_TOKEN_USER}:${DEPLOY_TOKEN}@gitlab.com/cloudfrontvinayak/juice-shop.git'" \
bash -s <<'EOF'
set -euo pipefail
# --- clone on first run, pull afterwards ---
if [ ! -d "$HOME/juice-shop/.git" ]; then
git clone "$REPO_URL" "$HOME/juice-shop"
else
git -C "$HOME/juice-shop" remote set-url origin "$REPO_URL"
git -C "$HOME/juice-shop" pull --ff-only
fi
# --- build and run ---
cd "$HOME/juice-shop"
npm install
if pm2 describe juice-shop >/dev/null 2>&1; then
pm2 reload juice-shop --update-env
else
PORT=3000 pm2 start npm --name juice-shop -- start
pm2 save
sudo env PATH="$PATH" pm2 startup systemd -u "$USER" --hp "$HOME" || true
fi
pm2 save
pm2 status
EOFstages: [deploy]
deploy_ec2:
stage: deploy
image: alpine:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
before_script:
- apk add --no-cache openssh-client
- chmod 600 "$SSH_PRIVATE_KEY"
script:
- |
ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=accept-new \
"$EC2_USER@$EC2_HOST" \
"REPO_URL='@gitlab.com/cloudfrontvinayak/juice-shop.git'">https://${DEPLOY_TOKEN_USER}:${DEPLOY_TOKEN}@gitlab.com/cloudfrontvinayak/juice-shop.git'" \
bash -s <<'EOF'
set -euo pipefail
# --- clone on first run, pull afterwards ---
if [ ! -d "$HOME/juice-shop/.git" ]; then
git clone "$REPO_URL" "$HOME/juice-shop"
else
git -C "$HOME/juice-shop" remote set-url origin "$REPO_URL"
git -C "$HOME/juice-shop" pull --ff-only
fi
# --- build and run ---
cd "$HOME/juice-shop"
npm install
if pm2 describe juice-shop >/dev/null 2>&1; then
pm2 reload juice-shop --update-env
else
PORT=3000 pm2 start npm --name juice-shop -- start
pm2 save
sudo env PATH="$PATH" pm2 startup systemd -u "$USER" --hp "$HOME" || true
fi
pm2 save
pm2 status
EOFSince we are using SSH based deployment, we need to open SSH access to EC2 instance from 0.0.0.0/0 for the deployment. Can remove it after the deployment is completed.
Once the deployment is completed
can access the application using EC2_PUBLIC_IP:3000
Step 3: Since we don't have a public domain name, we can create a private hosted zone and associate it with the VPC.
Now create a DNS record of type A record in this private hosted zone with the private IP of the EC2 instance.
Step 4: Follow https://docs.aws.amazon.com/securityagent/latest/userguide/vpc-interface-endpoints.html to create a VPC endpoint for security agent. Make sure that in the security group, port 443 and 3000 is accessible from VPC CIDR.
Step 5: Follow https://docs.aws.amazon.com/securityagent/latest/userguide/connect-gitlab.html to create GitLab integration.
Step 6: Now go to Security Agent and create an agent space and enable penetration test.
Once created, add GitLab integration for the penetration test and enable code remediation and code review comments.
Step 7: Now launch the web application and create a penetration test.
and select Gitlab integration and enable continuous pentesting and automatic code remediation.
Once created, start the pentest.
After sometime, you may see some findings
and a MR to fix the issue
But Mr Claude was not convinced with this change
Why it fails
With Sequelize replacements, :criteria is substituted with a quoted, escaped string literal. Inside the SQL it sits between quotes already:
LIKE '%:criteria%'
Sequelize doesn't replace :criteria inside a quoted string, because it's part of a literal. So the query searches for the literal text :criteria, not the user's input. The injection is gone, but so is the search.
Correct fix
Put the wildcards in the value, and leave the placeholder unquoted:
models.sequelize.query(
`SELECT * FROM Products
WHERE ((name LIKE :criteria OR description LIKE :criteria) AND deletedAt IS NULL)
ORDER BY name`,
{ replacements: { criteria: `%${criteria}%` } }
)
Sequelize then escapes the value and wraps it in quotes, so %foo% becomes '%foo%'.Why it fails
With Sequelize replacements, :criteria is substituted with a quoted, escaped string literal. Inside the SQL it sits between quotes already:
LIKE '%:criteria%'
Sequelize doesn't replace :criteria inside a quoted string, because it's part of a literal. So the query searches for the literal text :criteria, not the user's input. The injection is gone, but so is the search.
Correct fix
Put the wildcards in the value, and leave the placeholder unquoted:
models.sequelize.query(
`SELECT * FROM Products
WHERE ((name LIKE :criteria OR description LIKE :criteria) AND deletedAt IS NULL)
ORDER BY name`,
{ replacements: { criteria: `%${criteria}%` } }
)
Sequelize then escapes the value and wraps it in quotes, so %foo% becomes '%foo%'.I also got these notifications while accessing the application which suggest that pentest is working :)
Once the static analysis was executed for 5 agent hours, I got 2 findings and and 47 unverified findings which were still under verification and since it was going to take a long time, I decided to stop the penetration test.
Once the pentest is completed, you can generate the report.
Next I moved to execute threat modelling.
Note: Be mindful of the cost of running penetration test. Refer to https://aws.amazon.com/continuum/pricing/ for details.