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
5 changes: 4 additions & 1 deletion docker/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ fi
echo "Checking PyRIT installation..."
python -c "import pyrit; print(f'Running PyRIT version: {pyrit.__version__}')"

# Write .env file from PYRIT_ENV_CONTENTS (injected from Key Vault secret)
# Write .env file from PYRIT_ENV_CONTENTS (injected as the Container App's
# inline `env-file` secret; previously a Key Vault secretRef, but ACA isn't on
# Key Vault's "trusted services" list so SFI-locked-down KVs can't be read at
# runtime — see infra/main.bicep for details).
if [ -n "$PYRIT_ENV_CONTENTS" ]; then
mkdir -p ~/.pyrit
echo "$PYRIT_ENV_CONTENTS" > ~/.pyrit/.env
Expand Down
210 changes: 185 additions & 25 deletions gui-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@
# - copyrit-gui-common (azureServiceConnection, acrName, acrLoginServer, imageName)
# - copyrit-gui-test (resourceGroup, appName, entraTenantId, entraClientId,
# allowedGroupObjectIds, sqlServerFqdn, sqlDatabaseName,
# keyVaultResourceId, acrResourceId, enablePrivateEndpoint, enableOtel)
# keyVaultResourceId, acrResourceId, enablePrivateEndpoint, enableOtel,
# envFileContents [SECURE])
# - copyrit-gui-prod (same keys as test, with production values)
#
# The `envFileContents` variable must be marked SECURE in the ADO library.
# It contains the entire .env file (endpoints, keys, etc.) and is passed to
# the Bicep template as the @secure() `envFileContents` parameter via a
# parameters JSON file written to disk in the runner. Writing to a file
# (instead of `--parameters envFileContents=$(envFileContents)`) avoids
# exposing the value in the runner's process list / `ps` output.

trigger:
branches:
Expand Down Expand Up @@ -118,21 +126,97 @@ stages:
inlineScript: |
set -euo pipefail

# Write deployment parameters to a temp file so the
# @secure() envFileContents value never appears on the
# az CLI command line (visible in process list).
PARAMS_FILE="$(mktemp --suffix=.parameters.json)"
trap 'rm -f "$PARAMS_FILE"' EXIT

python3 - <<'PY' > "$PARAMS_FILE"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this need to be an inline script? Could it just be in the repo somewhere (scripts dir?)

import json, os, sys


def parse_bool(name: str) -> bool:
"""Strict bool parse: fail loudly if the ADO variable was unresolved.

ADO leaves unresolved macro variables as the literal string
'$(varname)'. Without this guard, that string would silently
coerce to False (since 'lower() == "true"' is False), which can
deploy with the wrong network posture (e.g., public access on
when private endpoint was intended). Fail loudly instead.
"""
val = os.environ.get(name, "")
if val.lower() in ("true", "false"):
return val.lower() == "true"
sys.exit(
f"ERROR: Pipeline variable {name} must be 'true' or 'false', "
f"got: {val!r}. Check the ADO variable group (likely the "
f"variable is missing or misspelled — ADO leaves unresolved "
f"macros as the literal string '$(name)')."
)


def parse_secret_str(name: str) -> str:
"""Fail loudly if a SECURE pipeline variable was unresolved.

Same root cause as parse_bool: ADO leaves unresolved macros as
'$(varname)'. For securestring parameters (envFileContents), Azure
accepts ANY string, so an unresolved macro silently deploys garbage
that the container can't parse. Detect the pattern and abort.
"""
val = os.environ.get(name, "")
# Match exactly '$(identifier)' — ADO macro syntax for unresolved variables.
if val.startswith("$(") and val.endswith(")") and "(" not in val[2:-1]:
sys.exit(
f"ERROR: Pipeline variable {name} appears unresolved (got "
f"'{val}'). The ADO library variable is likely missing or "
f"misspelled (ADO leaves unresolved macros as the literal "
f"string '$(name)'). For envFileContents, also confirm the "
f"variable is marked SECURE in the ADO library."
)
if not val:
sys.exit(f"ERROR: Pipeline variable {name} is empty.")
return val


doc = {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appName": {"value": os.environ["APP_NAME"]},
"containerImage": {"value": os.environ["CONTAINER_IMAGE"]},
"entraTenantId": {"value": os.environ["ENTRA_TENANT_ID"]},
"entraClientId": {"value": os.environ["ENTRA_CLIENT_ID"]},
"allowedGroupObjectIds": {"value": os.environ["ALLOWED_GROUP_OBJECT_IDS"]},
"sqlServerFqdn": {"value": os.environ["SQL_SERVER_FQDN"]},
"sqlDatabaseName": {"value": os.environ["SQL_DATABASE_NAME"]},
"keyVaultResourceId": {"value": os.environ["KEY_VAULT_RESOURCE_ID"]},
"acrResourceId": {"value": os.environ["ACR_RESOURCE_ID"]},
"enablePrivateEndpoint": {"value": parse_bool("ENABLE_PRIVATE_ENDPOINT")},
"enableOtel": {"value": parse_bool("ENABLE_OTEL")},
"envFileContents": {"value": parse_secret_str("ENV_FILE_CONTENTS")},
},
}
print(json.dumps(doc))
PY

az deployment group create \
--resource-group $(resourceGroup) \
--template-file $(Build.SourcesDirectory)/infra/main.bicep \
--parameters appName=$(appName) \
--parameters containerImage=$(acrLoginServer)/$(imageName):$(Build.SourceVersion) \
--parameters entraTenantId=$(entraTenantId) \
--parameters entraClientId=$(entraClientId) \
--parameters allowedGroupObjectIds="$(allowedGroupObjectIds)" \
--parameters sqlServerFqdn=$(sqlServerFqdn) \
--parameters sqlDatabaseName=$(sqlDatabaseName) \
--parameters keyVaultResourceId=$(keyVaultResourceId) \
--parameters acrResourceId=$(acrResourceId) \
--parameters enablePrivateEndpoint=$(enablePrivateEndpoint) \
--parameters enableOtel=$(enableOtel) \
--parameters envSecretName=$(envSecretName)
--parameters @"$PARAMS_FILE"
env:
APP_NAME: $(appName)
CONTAINER_IMAGE: $(acrLoginServer)/$(imageName):$(Build.SourceVersion)
ENTRA_TENANT_ID: $(entraTenantId)
ENTRA_CLIENT_ID: $(entraClientId)
ALLOWED_GROUP_OBJECT_IDS: $(allowedGroupObjectIds)
SQL_SERVER_FQDN: $(sqlServerFqdn)
SQL_DATABASE_NAME: $(sqlDatabaseName)
KEY_VAULT_RESOURCE_ID: $(keyVaultResourceId)
ACR_RESOURCE_ID: $(acrResourceId)
ENABLE_PRIVATE_ENDPOINT: $(enablePrivateEndpoint)
ENABLE_OTEL: $(enableOtel)
ENV_FILE_CONTENTS: $(envFileContents)

- task: AzureCLI@2
displayName: 'Health check'
Expand Down Expand Up @@ -189,21 +273,97 @@ stages:
inlineScript: |
set -euo pipefail

# Write deployment parameters to a temp file so the
# @secure() envFileContents value never appears on the
# az CLI command line (visible in process list).
PARAMS_FILE="$(mktemp --suffix=.parameters.json)"
trap 'rm -f "$PARAMS_FILE"' EXIT

python3 - <<'PY' > "$PARAMS_FILE"
import json, os, sys


def parse_bool(name: str) -> bool:
"""Strict bool parse: fail loudly if the ADO variable was unresolved.

ADO leaves unresolved macro variables as the literal string
'$(varname)'. Without this guard, that string would silently
coerce to False (since 'lower() == "true"' is False), which can
deploy with the wrong network posture (e.g., public access on
when private endpoint was intended). Fail loudly instead.
"""
val = os.environ.get(name, "")
if val.lower() in ("true", "false"):
return val.lower() == "true"
sys.exit(
f"ERROR: Pipeline variable {name} must be 'true' or 'false', "
f"got: {val!r}. Check the ADO variable group (likely the "
f"variable is missing or misspelled — ADO leaves unresolved "
f"macros as the literal string '$(name)')."
)


def parse_secret_str(name: str) -> str:
"""Fail loudly if a SECURE pipeline variable was unresolved.

Same root cause as parse_bool: ADO leaves unresolved macros as
'$(varname)'. For securestring parameters (envFileContents), Azure
accepts ANY string, so an unresolved macro silently deploys garbage
that the container can't parse. Detect the pattern and abort.
"""
val = os.environ.get(name, "")
# Match exactly '$(identifier)' — ADO macro syntax for unresolved variables.
if val.startswith("$(") and val.endswith(")") and "(" not in val[2:-1]:
sys.exit(
f"ERROR: Pipeline variable {name} appears unresolved (got "
f"'{val}'). The ADO library variable is likely missing or "
f"misspelled (ADO leaves unresolved macros as the literal "
f"string '$(name)'). For envFileContents, also confirm the "
f"variable is marked SECURE in the ADO library."
)
if not val:
sys.exit(f"ERROR: Pipeline variable {name} is empty.")
return val


doc = {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appName": {"value": os.environ["APP_NAME"]},
"containerImage": {"value": os.environ["CONTAINER_IMAGE"]},
"entraTenantId": {"value": os.environ["ENTRA_TENANT_ID"]},
"entraClientId": {"value": os.environ["ENTRA_CLIENT_ID"]},
"allowedGroupObjectIds": {"value": os.environ["ALLOWED_GROUP_OBJECT_IDS"]},
"sqlServerFqdn": {"value": os.environ["SQL_SERVER_FQDN"]},
"sqlDatabaseName": {"value": os.environ["SQL_DATABASE_NAME"]},
"keyVaultResourceId": {"value": os.environ["KEY_VAULT_RESOURCE_ID"]},
"acrResourceId": {"value": os.environ["ACR_RESOURCE_ID"]},
"enablePrivateEndpoint": {"value": parse_bool("ENABLE_PRIVATE_ENDPOINT")},
"enableOtel": {"value": parse_bool("ENABLE_OTEL")},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we doing otel? We should at some point, just didn't realize it's part of this PR

"envFileContents": {"value": parse_secret_str("ENV_FILE_CONTENTS")},
},
}
print(json.dumps(doc))
PY

az deployment group create \
--resource-group $(resourceGroup) \
--template-file $(Build.SourcesDirectory)/infra/main.bicep \
--parameters appName=$(appName) \
--parameters containerImage=$(acrLoginServer)/$(imageName):$(Build.SourceVersion) \
--parameters entraTenantId=$(entraTenantId) \
--parameters entraClientId=$(entraClientId) \
--parameters allowedGroupObjectIds="$(allowedGroupObjectIds)" \
--parameters sqlServerFqdn=$(sqlServerFqdn) \
--parameters sqlDatabaseName=$(sqlDatabaseName) \
--parameters keyVaultResourceId=$(keyVaultResourceId) \
--parameters acrResourceId=$(acrResourceId) \
--parameters enablePrivateEndpoint=$(enablePrivateEndpoint) \
--parameters enableOtel=$(enableOtel) \
--parameters envSecretName=$(envSecretName)
--parameters @"$PARAMS_FILE"
env:
APP_NAME: $(appName)
CONTAINER_IMAGE: $(acrLoginServer)/$(imageName):$(Build.SourceVersion)
ENTRA_TENANT_ID: $(entraTenantId)
ENTRA_CLIENT_ID: $(entraClientId)
ALLOWED_GROUP_OBJECT_IDS: $(allowedGroupObjectIds)
SQL_SERVER_FQDN: $(sqlServerFqdn)
SQL_DATABASE_NAME: $(sqlDatabaseName)
KEY_VAULT_RESOURCE_ID: $(keyVaultResourceId)
ACR_RESOURCE_ID: $(acrResourceId)
ENABLE_PRIVATE_ENDPOINT: $(enablePrivateEndpoint)
ENABLE_OTEL: $(enableOtel)
ENV_FILE_CONTENTS: $(envFileContents)

- task: AzureCLI@2
displayName: 'Health check'
Expand Down
Loading
Loading