Compare commits

...

9 Commits

Author SHA1 Message Date
Pablo F.G 5198b0251c chore(ui): add prettier-plugin-packagejson to enforce key ordering
Adds prettier-plugin-packagejson@2.5.22 as a devDependency and registers
it in .prettierrc.json before prettier-plugin-tailwindcss so prettier
keeps ui/package.json keys in the conventional npm order.

Reformatting package.json is a no-op functional change (key ordering
only). Verified with `pnpm exec prettier --check package.json` and
`pnpm run healthcheck`.

Refs: WebstormProjects-7b1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:44:15 +02:00
lydiavilchez 9894ac7bc3 feat(googleworkspace): implement Chat service with 6 CIS checks (#11126) 2026-05-14 17:19:11 +02:00
Pedro Martín 8ae076f22c fix(gcp): update duplicated CIS requirement ID (#11180) 2026-05-14 16:17:04 +02:00
Adrián Peña 27fb8518cd chore(sdk): pin root transitive deps to prevent silent drift (#11178) 2026-05-14 16:14:34 +02:00
Pepe Fagoaga 375de5dc87 docs: replace safety for osv-scanner (#11179) 2026-05-14 15:04:09 +02:00
Pepe Fagoaga 3410fc927a chore(security): replace safety with osv-scanner (#11167) 2026-05-14 14:35:09 +02:00
Alejandro Bailo dcf91ef252 feat(ui): add health check endpoint (#11145) 2026-05-14 13:47:48 +02:00
lydiavilchez bf4fd8fabd fix(googleworkspace): use per-service resources for Directory (#11176) 2026-05-14 13:07:06 +02:00
Alejandro Bailo 3d65208fd3 fix(ui): update vulnerable npm packages (#11173) 2026-05-14 12:55:29 +02:00
60 changed files with 6590 additions and 4639 deletions
+169
View File
@@ -0,0 +1,169 @@
name: 'OSV-Scanner'
description: 'Install osv-scanner and scan a lockfile, failing on HIGH/CRITICAL/UNKNOWN severity findings. Posts/updates a PR comment with findings on pull_request events (requires pull-requests: write).'
author: 'Prowler'
inputs:
lockfile:
description: 'Path to the lockfile to scan, relative to the repository root (e.g. uv.lock, api/uv.lock, ui/pnpm-lock.yaml).'
required: true
severity-levels:
description: 'Comma-separated severity levels that fail the scan. Default: HIGH,CRITICAL,UNKNOWN.'
required: false
default: 'HIGH,CRITICAL,UNKNOWN'
version:
description: 'osv-scanner release tag to install. When overriding, you MUST also override binary-sha256.'
required: false
default: 'v2.3.8'
binary-sha256:
description: 'Expected SHA256 of osv-scanner_linux_amd64 for the given version. Default tracks v2.3.8. See https://github.com/google/osv-scanner/releases/download/<version>/osv-scanner_SHA256SUMS.'
required: false
default: 'bc98e15319ed0d515e3f9235287ba53cdc5535d576d24fd573978ecfe9ab92dc'
post-pr-comment:
description: 'Post or update a PR comment with the scan report. Only effective on pull_request events. Requires pull-requests: write permission on the caller job.'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Install osv-scanner
shell: bash
env:
OSV_SCANNER_VERSION: ${{ inputs.version }}
# Download the binary AND the published SHA256SUMS file, then verify the
# binary checksum against the upstream-signed manifest. Aborts on mismatch.
run: |
set -euo pipefail
if command -v osv-scanner >/dev/null 2>&1; then
INSTALLED="$(osv-scanner --version 2>&1 | awk '/scanner version/ {print $NF; exit}')"
if [ "v${INSTALLED}" = "${OSV_SCANNER_VERSION}" ]; then
echo "osv-scanner ${OSV_SCANNER_VERSION} already installed."
exit 0
fi
fi
BASE="https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}"
BIN_NAME="osv-scanner_linux_amd64"
curl -fSL --retry 3 "${BASE}/${BIN_NAME}" -o "${RUNNER_TEMP}/${BIN_NAME}"
curl -fSL --retry 3 "${BASE}/osv-scanner_SHA256SUMS" -o "${RUNNER_TEMP}/osv-scanner_SHA256SUMS"
(cd "${RUNNER_TEMP}" && sha256sum --check --ignore-missing osv-scanner_SHA256SUMS)
chmod +x "${RUNNER_TEMP}/${BIN_NAME}"
sudo mv "${RUNNER_TEMP}/${BIN_NAME}" /usr/local/bin/osv-scanner
rm -f "${RUNNER_TEMP}/osv-scanner_SHA256SUMS"
osv-scanner --version
- name: Run osv-scanner
id: scan
shell: bash
working-directory: ${{ github.workspace }}
env:
OSV_LOCKFILE: ${{ inputs.lockfile }}
OSV_SEVERITY_LEVELS: ${{ inputs.severity-levels }}
OSV_REPORT_FILE: ${{ runner.temp }}/osv-scanner-findings.json
# Per-vulnerability ignores (reason + expiry) live in osv-scanner.toml at the repo root, if present.
# Severity filter is enforced in the wrapper via OSV_SEVERITY_LEVELS.
# `continue-on-error: true` lets the PR-comment step run even when findings exist;
# the gate step below re-fails the job from the wrapper exit code.
continue-on-error: true
run: ./.github/scripts/osv-scan.sh --lockfile="${OSV_LOCKFILE}"
- name: Post osv-scanner report on PR
if: >-
always()
&& inputs.post-pr-comment == 'true'
&& github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
OSV_REPORT_FILE: ${{ runner.temp }}/osv-scanner-findings.json
OSV_LOCKFILE: ${{ inputs.lockfile }}
OSV_SEVERITY_LEVELS: ${{ inputs.severity-levels }}
with:
script: |
const fs = require('fs');
const lockfile = process.env.OSV_LOCKFILE;
const severityLevels = process.env.OSV_SEVERITY_LEVELS;
const reportFile = process.env.OSV_REPORT_FILE;
const marker = `<!-- osv-scanner-report:${lockfile} -->`;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
let findings = [];
if (fs.existsSync(reportFile)) {
try {
findings = JSON.parse(fs.readFileSync(reportFile, 'utf8'));
} catch (err) {
core.warning(`Could not parse ${reportFile}: ${err.message}`);
return;
}
}
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body?.includes(marker));
if (findings.length === 0) {
if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
});
core.info(`Deleted stale osv-scanner comment for ${lockfile}.`);
} else {
core.info(`No findings and no stale comment for ${lockfile}.`);
}
return;
}
const sevIcon = (s) => ({
CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🟢', UNKNOWN: '⚪',
}[s] || '⚪');
const escape = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
const rows = findings.map(f =>
`| ${sevIcon(f.severity)} ${f.severity}${f.score ? ` (${f.score})` : ''} | \`${escape(f.id)}\` | \`${escape(f.ecosystem)}/${escape(f.package)}\` | \`${escape(f.version)}\` | ${escape(f.summary || '(no summary)')} |`
);
const body = [
marker,
`## 🔒 osv-scanner: ${findings.length} finding(s) in \`${lockfile}\``,
'',
`Severity gate: \`${severityLevels}\``,
'',
'| Severity | ID | Package | Version | Summary |',
'|----------|----|---------|---------|---------|',
...rows,
'',
`To accept a finding, add an \`[[IgnoredVulns]]\` entry to \`osv-scanner.toml\` at the repo root with a reason and \`ignoreUntil\`.`,
'',
`<sub>[View run](${runUrl})</sub>`,
].join('\n');
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated osv-scanner comment for ${lockfile}.`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
core.info(`Posted new osv-scanner comment for ${lockfile}.`);
}
- name: Enforce osv-scanner severity gate
shell: bash
env:
SCAN_OUTCOME: ${{ steps.scan.outcome }}
run: |
if [ "${SCAN_OUTCOME}" != "success" ]; then
echo "osv-scanner gate: scan reported findings (outcome=${SCAN_OUTCOME})" >&2
exit 1
fi
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Run osv-scanner and fail when findings match the configured severity levels.
#
# Replaces `safety check --policy-file .safety-policy.yml`. Used by:
# - .github/actions/osv-scanner/action.yml (composite action)
# - .github/workflows/api-security.yml, sdk-security.yml, ui-security.yml
#
# Severity levels (comma-separated) are read from OSV_SEVERITY_LEVELS.
# Default: HIGH,CRITICAL,UNKNOWN — preserves prior .safety-policy.yml policy
# (ignore-cvss-severity-below: 7 + ignore-cvss-unknown-severity: False).
# osv-scanner has no native CVSS threshold (google/osv-scanner#1400, closed
# not-planned). Severity is derived from $group.max_severity (numeric CVSS
# score string) which osv-scanner emits per group.
#
# CVSS v3 score → categorical label:
# CRITICAL >= 9.0
# HIGH >= 7.0
# MEDIUM >= 4.0
# LOW > 0.0
# UNKNOWN no score available
#
# Per-vulnerability ignores (with reason + expiry) live in osv-scanner.toml at
# the repo root, if it exists. Without that file, osv-scanner uses defaults.
#
# Usage:
# osv-scan.sh [osv-scanner pass-through args...]
# Examples:
# osv-scan.sh --lockfile=uv.lock
# osv-scan.sh --recursive .
# OSV_SEVERITY_LEVELS=CRITICAL osv-scan.sh --lockfile=uv.lock
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
CONFIG="${ROOT}/osv-scanner.toml"
SEVERITY_LEVELS="${OSV_SEVERITY_LEVELS:-HIGH,CRITICAL,UNKNOWN}"
for bin in osv-scanner jq; do
if ! command -v "${bin}" >/dev/null 2>&1; then
echo "error: ${bin} not found in PATH" >&2
exit 2
fi
done
SCAN_ARGS=()
if [ -f "${CONFIG}" ]; then
SCAN_ARGS+=(--config="${CONFIG}")
fi
# Exit codes: 0=clean, 1=findings, 127=no supported files, 128=internal error.
STDERR="$(mktemp)"
trap 'rm -f "${STDERR}"' EXIT
set +e
OUTPUT="$(osv-scanner scan source "${SCAN_ARGS[@]}" --format=json "$@" 2>"${STDERR}")"
RC=$?
set -e
case "${RC}" in
0|1) ;;
127) echo "osv-scanner: no supported lockfiles in scan target"; exit 0 ;;
*)
echo "osv-scanner: exited with code ${RC}" >&2
[ -s "${STDERR}" ] && cat "${STDERR}" >&2
exit "${RC}"
;;
esac
# Build a JSON array of normalized severity levels for jq.
SEVERITY_JSON="$(printf '%s' "${SEVERITY_LEVELS}" | jq -Rc '
split(",") | map(ascii_upcase | sub("^\\s+"; "") | sub("\\s+$"; ""))
')"
# Walk each vulnerability, look up its group's max_severity (numeric CVSS),
# map to a categorical label, then filter by OSV_SEVERITY_LEVELS.
FINDINGS="$(printf '%s' "${OUTPUT}" | jq --argjson sevs "${SEVERITY_JSON}" '
[ .results[]?.packages[]?
| . as $pkg
| ($pkg.groups // []) as $groups
| ($pkg.vulnerabilities // [])[]
| . as $vuln
| ([ $groups[] | select((.ids // []) | index($vuln.id)) ][0] // {}) as $group
| (($group.max_severity // "") | tonumber? // null) as $score
| (if $score == null then "UNKNOWN"
elif $score >= 9.0 then "CRITICAL"
elif $score >= 7.0 then "HIGH"
elif $score >= 4.0 then "MEDIUM"
elif $score > 0 then "LOW"
else "UNKNOWN"
end) as $label
| {
id: $vuln.id,
severity: $label,
score: $score,
summary: ($vuln.summary // null),
package: $pkg.package.name,
version: $pkg.package.version,
ecosystem: $pkg.package.ecosystem
}
| select(.severity as $s | $sevs | any(. == $s))
]
')"
COUNT="$(printf '%s' "${FINDINGS}" | jq 'length')"
# Write the findings JSON to OSV_REPORT_FILE so callers (e.g. the composite
# action's PR-comment step) can consume the same data the gate decision uses.
if [ -n "${OSV_REPORT_FILE:-}" ]; then
printf '%s' "${FINDINGS}" > "${OSV_REPORT_FILE}"
fi
if [ "${COUNT}" -gt 0 ]; then
echo "osv-scanner: ${COUNT} finding(s) at severity ${SEVERITY_LEVELS}"
printf '%s' "${FINDINGS}" | jq -r '
.[] | " [\(.severity)\(if .score then " \(.score)" else "" end)] \(.id) \(.ecosystem)/\(.package)@\(.version) — \(.summary // "(no summary)")"
'
echo
echo "To accept a finding, create osv-scanner.toml at the repo root with a reason and ignoreUntil."
exit 1
fi
echo "osv-scanner: no findings at severity levels: ${SEVERITY_LEVELS}"
+18 -9
View File
@@ -10,6 +10,8 @@ on:
- '.github/workflows/api-tests.yml'
- '.github/workflows/api-security.yml'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
pull_request:
branches:
- "master"
@@ -19,6 +21,8 @@ on:
- '.github/workflows/api-tests.yml'
- '.github/workflows/api-security.yml'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -35,6 +39,7 @@ jobs:
timeout-minutes: 15
permissions:
contents: read
pull-requests: write # osv-scanner action posts/updates a PR comment with findings
strategy:
matrix:
python-version:
@@ -52,11 +57,12 @@ jobs:
pypi.org:443
files.pythonhosted.org:443
github.com:443
auth.safetycli.com:443
pyup.io:443
raw.githubusercontent.com:443
data.safetycli.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
api.osv.dev:443
api.deps.dev:443
osv-vulnerabilities.storage.googleapis.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -71,7 +77,8 @@ jobs:
files: |
api/**
.github/workflows/api-security.yml
.safety-policy.yml
.github/actions/osv-scanner/**
.github/scripts/osv-scan.sh
files_ignore: |
api/docs/**
api/README.md
@@ -91,11 +98,13 @@ jobs:
# bandit would recurse into installed third-party packages.
run: uv run bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .
- name: Safety
- name: Dependency vulnerability scan with osv-scanner
if: steps.check-changes.outputs.any_changed == 'true'
# Accepted CVEs, severity threshold, and ignore expirations live in ../.safety-policy.yml
run: uv run safety check --policy-file ../.safety-policy.yml
uses: ./.github/actions/osv-scanner
with:
lockfile: api/uv.lock
- name: Vulture
if: steps.check-changes.outputs.any_changed == 'true'
# Run even when osv-scanner reports findings so dead-code signal isn't masked by SCA failures.
if: ${{ !cancelled() && steps.check-changes.outputs.any_changed == 'true' }}
run: uv run vulture --exclude "contrib,tests,conftest.py,.venv" --min-confidence 100 .
+16 -7
View File
@@ -13,6 +13,8 @@ on:
- '.github/workflows/sdk-tests.yml'
- '.github/workflows/sdk-security.yml'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
pull_request:
branches:
- 'master'
@@ -25,6 +27,8 @@ on:
- '.github/workflows/sdk-tests.yml'
- '.github/workflows/sdk-security.yml'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -39,6 +43,7 @@ jobs:
timeout-minutes: 15
permissions:
contents: read
pull-requests: write # osv-scanner action posts/updates a PR comment with findings
steps:
- name: Harden Runner
@@ -49,10 +54,12 @@ jobs:
pypi.org:443
files.pythonhosted.org:443
github.com:443
auth.safetycli.com:443
pyup.io:443
data.safetycli.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
api.osv.dev:443
api.deps.dev:443
osv-vulnerabilities.storage.googleapis.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -97,11 +104,13 @@ jobs:
if: steps.check-changes.outputs.any_changed == 'true'
run: uv run bandit -q -lll -x '*_test.py,./.venv/,./contrib/,./api/,./ui' -r .
- name: Security scan with Safety
- name: Dependency vulnerability scan with osv-scanner
if: steps.check-changes.outputs.any_changed == 'true'
# Accepted CVEs, severity threshold, and ignore expirations live in .safety-policy.yml
run: uv run safety check -r pyproject.toml --policy-file .safety-policy.yml
uses: ./.github/actions/osv-scanner
with:
lockfile: uv.lock
- name: Dead code detection with Vulture
if: steps.check-changes.outputs.any_changed == 'true'
# Run even when osv-scanner reports findings so dead-code signal isn't masked by SCA failures.
if: ${{ !cancelled() && steps.check-changes.outputs.any_changed == 'true' }}
run: uv run vulture --exclude ".venv,contrib,api,ui" --min-confidence 100 .
+75
View File
@@ -0,0 +1,75 @@
name: 'UI: Security'
on:
push:
branches:
- 'master'
- 'v5.*'
paths:
- 'ui/package.json'
- 'ui/pnpm-lock.yaml'
- '.github/workflows/ui-security.yml'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
pull_request:
branches:
- 'master'
- 'v5.*'
paths:
- 'ui/package.json'
- 'ui/pnpm-lock.yaml'
- '.github/workflows/ui-security.yml'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
ui-security-scans:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write # osv-scanner action posts/updates a PR comment with findings
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: block
allowed-endpoints: >
github.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
api.osv.dev:443
api.deps.dev:443
osv-vulnerabilities.storage.googleapis.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for UI dependency changes
id: check-changes
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
with:
files: |
ui/package.json
ui/pnpm-lock.yaml
.github/workflows/ui-security.yml
.github/actions/osv-scanner/**
.github/scripts/osv-scan.sh
- name: Dependency vulnerability scan with osv-scanner
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/osv-scanner
with:
lockfile: ui/pnpm-lock.yaml
-10
View File
@@ -165,16 +165,6 @@ repos:
exclude: { glob: ["{contrib,skills}/**", "**/.venv/**", "**/*_test.py"] }
priority: 40
- id: safety
name: safety
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
# Accepted CVEs, severity threshold, and ignore expirations live in .safety-policy.yml
entry: safety check --policy-file .safety-policy.yml
language: system
pass_filenames: false
files: { glob: ["**/pyproject.toml", "**/uv.lock", "**/requirements*.txt", ".safety-policy.yml"] }
priority: 40
- id: vulture
name: vulture
description: "Vulture finds unused code in Python programs."
-58
View File
@@ -1,58 +0,0 @@
# Safety policy for `safety check` (Safety CLI 3.x, v2 schema).
# Applied in: .pre-commit-config.yaml, .github/workflows/api-security.yml,
# .github/workflows/sdk-security.yml via `--policy-file`.
#
# Validate: uv run safety validate policy_file --path .safety-policy.yml
security:
# Scan unpinned requirements too. Prowler pins via uv.lock, so this is
# defensive against accidental unpinned entries.
ignore-unpinned-requirements: False
# CVSS severity filter. 7 = report only HIGH (7.08.9) and CRITICAL (9.010.0).
# Reference: 9=CRITICAL only, 7=CRITICAL+HIGH, 4=CRITICAL+HIGH+MEDIUM.
ignore-cvss-severity-below: 7
# Unknown severity is unrated, not safe. Keep False so unrated CVEs still fail
# the build and get a human eye. Flip to True only if noise is unmanageable.
ignore-cvss-unknown-severity: False
# Fail the build when a non-ignored vulnerability is found.
continue-on-vulnerability-error: False
# Explicit accepted vulnerabilities. Each entry MUST have a reason and an
# expiry. Expired entries fail the scan, forcing re-audit.
ignore-vulnerabilities:
77744:
reason: "Botocore requires urllib3 1.X. Remove once upgraded to urllib3 2.X."
expires: '2026-10-22'
77745:
reason: "Botocore requires urllib3 1.X. Remove once upgraded to urllib3 2.X."
expires: '2026-10-22'
79023:
reason: "knack ReDoS; blocked until azure-cli-core (via cartography) allows knack >=0.13.0."
expires: '2026-10-22'
79027:
reason: "knack ReDoS; blocked until azure-cli-core (via cartography) allows knack >=0.13.0."
expires: '2026-10-22'
86217:
reason: "alibabacloud-tea-openapi==0.4.3 blocks upgrade to cryptography >=46.0.0."
expires: '2026-10-22'
71600:
reason: "CVE-2024-1135 false positive. Fixed in gunicorn 22.0.0; project uses 23.0.0."
expires: '2026-10-22'
70612:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
66963:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
74429:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
76352:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
76353:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
+1 -1
View File
@@ -167,7 +167,7 @@ runs:
- name: Upload SARIF to GitHub Code Scanning
if: always() && inputs.upload-sarif == 'true' && steps.find-sarif.outputs.sarif_path != ''
uses: github/codeql-action/upload-sarif@d4b3ca9fa7f69d38bfcd667bdc45bc373d16277e # v4
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
sarif_file: ${{ steps.find-sarif.outputs.sarif_path }}
category: ${{ inputs.sarif-category }}
-3
View File
@@ -15,7 +15,6 @@ dev = [
"pytest-randomly==3.15.0",
"pytest-xdist==3.6.1",
"ruff==0.5.0",
"safety==3.7.0",
"tqdm==4.67.1",
"vulture==2.14",
"prek==0.3.9"
@@ -393,8 +392,6 @@ constraint-dependencies = [
"ruamel-yaml==0.19.1",
"ruff==0.5.0",
"s3transfer==0.14.0",
"safety==3.7.0",
"safety-schemas==0.0.16",
"scaleway==2.10.3",
"scaleway-core==2.10.3",
"schema==0.7.5",
Generated
-145
View File
@@ -329,8 +329,6 @@ constraints = [
{ name = "ruamel-yaml", specifier = "==0.19.1" },
{ name = "ruff", specifier = "==0.5.0" },
{ name = "s3transfer", specifier = "==0.14.0" },
{ name = "safety", specifier = "==3.7.0" },
{ name = "safety-schemas", specifier = "==0.0.16" },
{ name = "scaleway", specifier = "==2.10.3" },
{ name = "scaleway-core", specifier = "==2.10.3" },
{ name = "schema", specifier = "==0.7.5" },
@@ -1030,18 +1028,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "authlib"
version = "1.6.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" },
]
[[package]]
name = "autopep8"
version = "2.3.2"
@@ -2511,18 +2497,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/80/12235e5b75bb2c586733280854f131b86051e0bbdfb55349ff70d0f72cf9/dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d", size = 64447, upload-time = "2025-10-11T17:35:38.573Z" },
]
[[package]]
name = "dparse"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" },
]
[[package]]
name = "drf-extensions"
version = "0.8.0"
@@ -3318,15 +3292,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
[[package]]
name = "joblib"
version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "jsonpatch"
version = "1.33"
@@ -3990,21 +3955,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "nltk"
version = "3.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
]
[[package]]
name = "numpy"
version = "2.0.2"
@@ -4596,7 +4546,6 @@ dev = [
{ name = "pytest-randomly" },
{ name = "pytest-xdist" },
{ name = "ruff" },
{ name = "safety" },
{ name = "tqdm" },
{ name = "vulture" },
]
@@ -4661,7 +4610,6 @@ dev = [
{ name = "pytest-randomly", specifier = "==3.15.0" },
{ name = "pytest-xdist", specifier = "==3.6.1" },
{ name = "ruff", specifier = "==0.5.0" },
{ name = "safety", specifier = "==3.7.0" },
{ name = "tqdm", specifier = "==4.67.1" },
{ name = "vulture", specifier = "==2.14" },
]
@@ -5257,46 +5205,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "regex"
version = "2026.1.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" },
{ url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" },
{ url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" },
{ url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" },
{ url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" },
{ url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" },
{ url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" },
{ url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" },
{ url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" },
{ url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" },
{ url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
{ url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
{ url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
{ url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
{ url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
{ url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
{ url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
{ url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
{ url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
{ url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
{ url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
{ url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
{ url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
]
[[package]]
name = "reportlab"
version = "4.4.10"
@@ -5448,15 +5356,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
]
[[package]]
name = "ruff"
version = "0.5.0"
@@ -5494,50 +5393,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
]
[[package]]
name = "safety"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
{ name = "click" },
{ name = "dparse" },
{ name = "filelock" },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "marshmallow" },
{ name = "nltk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "ruamel-yaml" },
{ name = "safety-schemas" },
{ name = "tenacity" },
{ name = "tomlkit" },
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" },
]
[[package]]
name = "safety-schemas"
version = "0.0.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dparse" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "ruamel-yaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" },
]
[[package]]
name = "scaleway"
version = "2.10.3"
+7 -1
View File
@@ -48,10 +48,16 @@ services:
- path: .env
required: false
ports:
- ${UI_PORT:-3000}:${UI_PORT:-3000}
- ${UI_PORT:-3000}:3000
depends_on:
mcp-server:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
postgres:
image: postgres:16.3-alpine3.20
+1 -1
View File
@@ -152,7 +152,7 @@ These should have been already installed if `uv sync` was already run.
</Note>
- [`bandit`](https://pypi.org/project/bandit/) for code security review.
- [`safety`](https://pypi.org/project/safety/) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`osv-scanner`](https://github.com/google/osv-scanner) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`hadolint`](https://github.com/hadolint/hadolint) and [`dockle`](https://github.com/goodwithtech/dockle) for container security.
- [`Snyk`](https://docs.snyk.io/integrations/snyk-container-integrations/container-security-with-docker-hub-integration) for container security in Docker Hub.
- [`clair`](https://github.com/quay/clair) for container security in Amazon ECR.
+5 -4
View File
@@ -39,10 +39,11 @@ Dependencies are continuously monitored for known vulnerabilities with timely up
### Dependency Vulnerability Scanning
- **Safety:** Scans Python dependencies against known vulnerability databases
- Runs on every commit via pre-commit hooks
- Integrated into CI/CD for SDK and API
- Configured with selective ignores for tracked exceptions
- **osv-scanner:** Scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database
- Runs in CI on every pull request and push for SDK, API, and UI
- Fails the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings
- Posts a per-lockfile report as a PR comment
- Per-vulnerability ignores (with reason and expiry) live in `osv-scanner.toml` at the repo root
- **Trivy:** Multi-purpose scanner for containers and dependencies
- Scans all container images (UI, API, SDK, MCP Server)
- Checks for vulnerabilities in OS packages and application dependencies
@@ -18,7 +18,7 @@ Prowler requests the following read-only OAuth 2.0 scopes:
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
| `https://www.googleapis.com/auth/admin.directory.orgunit.readonly` | Read access to organizational unit hierarchy (identifies the root OU for policy filtering) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar service checks) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Gmail, Chat, and Drive service checks) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
<Warning>
@@ -40,7 +40,7 @@ In the [Google Cloud Console](https://console.cloud.google.com), select the targ
| API | Required For |
|-----|--------------|
| **Admin SDK API** | Directory service checks (users, roles, domains) |
| **Cloud Identity API** | Calendar service checks (domain-level sharing and invitation policies) |
| **Cloud Identity API** | Calendar, Gmail, Chat, and Drive service checks (domain-level application policies) |
For each API:
@@ -49,7 +49,7 @@ For each API:
3. Click **Enable**
<Note>
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar checks will return no findings if the Cloud Identity API is not enabled.
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar, Gmail, Chat, and Drive checks will return no findings if the Cloud Identity API is not enabled.
</Note>
### Step 3: Create a Service Account
@@ -176,9 +176,9 @@ If Prowler connects but returns empty results or permission errors for specific
- Verify all scopes are authorized in the Admin Console
- Ensure the delegated user is an active super administrator
### Calendar Checks Return No Findings
### Policy API Checks Return No Findings
If the Directory checks run successfully but the Calendar checks (e.g., `calendar_external_sharing_primary_calendar`) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
If the Directory checks run successfully but the Calendar, Gmail, Chat, or Drive checks return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
- The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2)
- The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5)
+3
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- 6 Chat file sharing, external messaging, spaces, and apps access checks for Google Workspace provider using the Cloud Identity Policy API [(#11126)](https://github.com/prowler-cloud/prowler/pull/11126)
- `entra_service_principal_no_secrets_for_permanent_tier0_roles` check for M365 provider [(#10788)](https://github.com/prowler-cloud/prowler/pull/10788)
- `iam_user_access_not_stale_to_sagemaker` check for AWS provider with configurable `max_unused_sagemaker_access_days` (default 90) [(#11000)](https://github.com/prowler-cloud/prowler/pull/11000)
- `cloudtrail_bedrock_logging_enabled` check for AWS provider [(#10858)](https://github.com/prowler-cloud/prowler/pull/10858)
@@ -19,6 +20,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🐞 Fixed
- Google Workspace Directory checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11176)](https://github.com/prowler-cloud/prowler/pull/11176)
- Google Workspace Calendar and Drive services sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11161)](https://github.com/prowler-cloud/prowler/pull/11161)
- `zone_waf_enabled` check for Cloudflare provider now appends a plan-aware hint to the FAIL `status_extended`: a possible-false-positive note on paid plans (Pro, Business, Enterprise) where the legacy `waf` zone setting can read `off` even though WAF managed rulesets are deployed via the dashboard, and a "not available on the Cloudflare Free plan" note on Free zones [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896)
- Google Workspace Gmail checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11169)](https://github.com/prowler-cloud/prowler/pull/11169)
@@ -30,6 +32,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🐞 Fixed
- `entra_users_mfa_capable` and `entra_break_glass_account_fido2_security_key_registered` report a preventive FAIL per affected user (with the missing permission named) when the M365 service principal lacks `AuditLog.Read.All`, instead of mass false positives [(#10907)](https://github.com/prowler-cloud/prowler/pull/10907)
- Update duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180)
---
+2 -2
View File
@@ -914,7 +914,7 @@
]
},
{
"Id": "3.1",
"Id": "3.10",
"Description": "Use Identity Aware Proxy (IAP) to Ensure Only Traffic From Google IP Addresses are 'Allowed'",
"Checks": [],
"Attributes": [
@@ -1132,7 +1132,7 @@
]
},
{
"Id": "4.1",
"Id": "4.10",
"Description": "Ensure That App Engine Applications Enforce HTTPS Connections",
"Checks": [],
"Attributes": [
@@ -1084,7 +1084,9 @@
{
"Id": "3.1.4.1.1",
"Description": "Ensure external filesharing in Google Chat and Hangouts is disabled",
"Checks": [],
"Checks": [
"chat_external_file_sharing_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1105,7 +1107,9 @@
{
"Id": "3.1.4.1.2",
"Description": "Ensure internal filesharing in Google Chat and Hangouts is disabled",
"Checks": [],
"Checks": [
"chat_internal_file_sharing_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1126,7 +1130,9 @@
{
"Id": "3.1.4.2.1",
"Description": "Ensure Google Chat externally is restricted to allowed domains",
"Checks": [],
"Checks": [
"chat_external_messaging_restricted"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1147,7 +1153,9 @@
{
"Id": "3.1.4.3.1",
"Description": "Ensure external spaces in Google Chat and Hangouts are restricted",
"Checks": [],
"Checks": [
"chat_external_spaces_restricted"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1168,7 +1176,9 @@
{
"Id": "3.1.4.4.1",
"Description": "Ensure allow users to install Chat apps is disabled",
"Checks": [],
"Checks": [
"chat_apps_installation_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1189,7 +1199,9 @@
{
"Id": "3.1.4.4.2",
"Description": "Ensure allow users to add and use incoming webhooks is disabled",
"Checks": [],
"Checks": [
"chat_incoming_webhooks_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1466,7 +1466,9 @@
{
"Id": "GWS.CHAT.2.1",
"Description": "External file sharing SHALL be disabled to protect sensitive information from unauthorized or accidental sharing",
"Checks": [],
"Checks": [
"chat_external_file_sharing_disabled"
],
"Attributes": [
{
"Section": "Chat",
@@ -1492,7 +1494,9 @@
{
"Id": "GWS.CHAT.4.1",
"Description": "External chat messaging SHALL be restricted to allowlisted domains only",
"Checks": [],
"Checks": [
"chat_external_messaging_restricted"
],
"Attributes": [
{
"Section": "Chat",
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_apps_installation_disabled",
"CheckTitle": "Chat apps installation is disabled for users",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat apps connect to external services to look up information, schedule meetings, or complete tasks. Apps are accounts created by Google, users in the organization, or third parties that can access user data including **email addresses**, **conversation content**, and **organizational information**.",
"Risk": "Unrestricted Chat app installation allows **unvetted third-party applications** to access user data including conversation content and organizational information. An attacker could distribute a malicious Chat app to **exfiltrate confidential data** or establish **persistent access** to internal communications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/6089179",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to install Chat apps** to **OFF**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable Chat apps installation to prevent **unvetted third-party applications** from accessing organizational data through the Chat platform.",
"Url": "https://hub.prowler.com/check/chat_apps_installation_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_incoming_webhooks_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_apps_installation_disabled(Check):
"""Check that users cannot install Chat apps.
This check verifies that the domain-level Chat policy prevents users
from installing Chat apps, reducing the risk of data exposure through
third-party or unvetted applications.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
apps_enabled = chat_client.policies.enable_apps
if apps_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Chat apps installation is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif apps_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Chat apps installation uses Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Chat apps installation is enabled "
f"in domain {chat_client.provider.identity.domain}. "
f"Chat apps installation should be disabled to prevent unvetted apps."
)
findings.append(report)
return findings
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.googleworkspace.services.chat.chat_service import Chat
chat_client = Chat(Provider.get_global_provider())
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_file_sharing_disabled",
"CheckTitle": "External file sharing in Chat is set to no files",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external file sharing** controls whether users can share files with people outside the organization via Chat conversations. Files often contain **confidential information**, and organizations in regulated industries need to control the flow of this information outside their boundaries.",
"Risk": "Enabled external file sharing allows users to send files containing **confidential information** to external parties through Chat. This creates a **data leakage** channel that bypasses DLP controls, particularly dangerous for organizations handling **regulated data** such as PII, PHI, or financial records.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **External filesharing** to **No files**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **external file sharing** in Chat to prevent users from sharing files with people outside the organization through Chat conversations.",
"Url": "https://hub.prowler.com/check/chat_external_file_sharing_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_internal_file_sharing_disabled",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_file_sharing_disabled(Check):
"""Check that external file sharing in Google Chat is disabled.
This check verifies that the domain-level Chat policy prevents users
from sharing files with people outside the organization via Chat,
protecting sensitive information from unauthorized external access.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
external_sharing = chat_client.policies.external_file_sharing
if external_sharing == "NO_FILES":
report.status = "PASS"
report.status_extended = (
f"External file sharing in Chat is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if external_sharing is None:
report.status_extended = (
f"External file sharing in Chat is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"External file sharing should be set to No files."
)
else:
report.status_extended = (
f"External file sharing in Chat is set to {external_sharing} "
f"in domain {chat_client.provider.identity.domain}. "
f"External file sharing should be set to No files."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_messaging_restricted",
"CheckTitle": "External Chat messaging is restricted to allowed domains",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external messaging** controls whether users can send messages to people outside the organization. If external messaging is allowed, it can optionally be restricted to only **allowlisted domains** to limit the scope of external communication.",
"Risk": "Unrestricted external messaging allows users to communicate freely with **any external party**, increasing the risk of **data exfiltration** through conversation content and **social engineering attacks** from untrusted domains targeting internal users.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Chat Settings**\n4. Select **Chat externally**\n5. Set **Allow users to send messages outside the organization** to **ON**\n6. Check **Only allow this for allowlisted domains**\n7. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **external Chat messaging** to **allowlisted domains** only to limit information flow to trusted parties and reduce exposure to external threats.",
"Url": "https://hub.prowler.com/check/chat_external_messaging_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_spaces_restricted",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_messaging_restricted(Check):
"""Check that external Chat messaging is restricted to allowed domains.
This check verifies that external Chat messaging is either disabled
entirely or restricted to allowlisted domains only, preventing
unrestricted communication with external users.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
allow_external = chat_client.policies.allow_external_chat
restriction = chat_client.policies.external_chat_restriction
if allow_external is False:
report.status = "PASS"
report.status_extended = (
f"External Chat messaging is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif allow_external is None and restriction is None:
report.status = "PASS"
report.status_extended = (
f"External Chat messaging uses Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
elif restriction == "TRUSTED_DOMAINS":
report.status = "PASS"
report.status_extended = (
f"External Chat messaging is restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"External Chat messaging is not restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}. "
f"External messaging should be restricted to allowed domains only."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_spaces_restricted",
"CheckTitle": "External spaces in Chat are restricted to allowed domains",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external spaces** allow users to create or join collaborative spaces that include people outside the organization. If external spaces are allowed, they can optionally be restricted to only **allowlisted domains** to limit external participation.",
"Risk": "Unrestricted external spaces allow users to add **anyone from any domain** to persistent group conversations. This increases the risk of **confidential information exposure** in shared spaces and enables **unauthorized external access** to ongoing organizational discussions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Spaces**\n4. Set **Allow users to create and join spaces with people outside their organization** to **ON**\n5. Check **Only allow users to add people from allowlisted domains**\n6. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **external spaces** to **allowlisted domains** only to control which external parties can participate in organizational Chat spaces.",
"Url": "https://hub.prowler.com/check/chat_external_spaces_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_messaging_restricted",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_spaces_restricted(Check):
"""Check that external spaces in Google Chat are restricted.
This check verifies that external spaces are either disabled entirely
or restricted to allowlisted domains only, preventing users from
creating or joining spaces with unrestricted external participants.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
spaces_enabled = chat_client.policies.external_spaces_enabled
allowlist_mode = chat_client.policies.external_spaces_domain_allowlist_mode
if spaces_enabled is False:
report.status = "PASS"
report.status_extended = (
f"External spaces are disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif allowlist_mode == "TRUSTED_DOMAINS":
report.status = "PASS"
report.status_extended = (
f"External spaces are restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if spaces_enabled is None and allowlist_mode is None:
report.status_extended = (
f"External spaces restriction is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"External spaces should be restricted to allowed domains only."
)
else:
report.status_extended = (
f"External spaces are not restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}. "
f"External spaces should be restricted to allowed domains only."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_incoming_webhooks_disabled",
"CheckTitle": "Incoming webhooks in Chat are disabled for users",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "**Incoming webhooks** let external applications post asynchronous messages into Google Chat spaces without being a Chat app. When enabled, users can configure webhooks and developers can call them to send content from **external applications**.",
"Risk": "Exposed webhook URLs allow **unauthorized content injection** into Chat spaces. Attackers can send **fraudulent or misleading messages** that appear to come from trusted services, creating a vector for **social engineering** and **phishing** within internal communications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/6089179",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to add and use incoming webhooks** to **OFF**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **incoming webhooks** to prevent unauthenticated external applications from **injecting content** into internal Chat spaces.",
"Url": "https://hub.prowler.com/check/chat_incoming_webhooks_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_apps_installation_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_incoming_webhooks_disabled(Check):
"""Check that incoming webhooks are disabled in Google Chat.
This check verifies that the domain-level Chat policy prevents users
from adding and using incoming webhooks, reducing the risk of
unauthorized content being posted into Chat spaces.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
webhooks_enabled = chat_client.policies.enable_webhooks
if webhooks_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Incoming webhooks are disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif webhooks_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Incoming webhooks use Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Incoming webhooks are enabled "
f"in domain {chat_client.provider.identity.domain}. "
f"Incoming webhooks should be disabled to prevent unauthorized content."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_internal_file_sharing_disabled",
"CheckTitle": "Internal file sharing in Chat is set to no files",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **internal file sharing** controls whether users can share files with other people inside the organization via Chat conversations. Organizations in regulated industries may need to **control and audit** all file sharing, even between internal users.",
"Risk": "Unrestricted internal file sharing in Chat allows files with **sensitive information** to be distributed freely without passing through approved channels. This undermines **data governance** and **audit trail** requirements, making it harder to track data movement within the organization.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **Internal filesharing** to **No files**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **internal file sharing** in Chat to enforce file distribution through **approved channels** with proper audit trails and governance controls.",
"Url": "https://hub.prowler.com/check/chat_internal_file_sharing_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_file_sharing_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_internal_file_sharing_disabled(Check):
"""Check that internal file sharing in Google Chat is disabled.
This check verifies that the domain-level Chat policy prevents users
from sharing files internally via Chat, providing maximum control over
file distribution within the organization.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
internal_sharing = chat_client.policies.internal_file_sharing
if internal_sharing == "NO_FILES":
report.status = "PASS"
report.status_extended = (
f"Internal file sharing in Chat is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if internal_sharing is None:
report.status_extended = (
f"Internal file sharing in Chat is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"Internal file sharing should be set to No files."
)
else:
report.status_extended = (
f"Internal file sharing in Chat is set to {internal_sharing} "
f"in domain {chat_client.provider.identity.domain}. "
f"Internal file sharing should be set to No files."
)
findings.append(report)
return findings
@@ -0,0 +1,125 @@
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService
class Chat(GoogleWorkspaceService):
"""Google Workspace Chat service for auditing domain-level Chat policies.
Uses the Cloud Identity Policy API v1 to read Chat file sharing, external
messaging, spaces, and apps access settings configured in the Admin Console.
"""
def __init__(self, provider):
super().__init__(provider)
self.policies = ChatPolicies()
self.policies_fetched = False
self._fetch_chat_policies()
def _fetch_chat_policies(self):
"""Fetch Chat policies from the Cloud Identity Policy API v1."""
logger.info("Chat - Fetching Chat policies...")
try:
service = self._build_service("cloudidentity", "v1")
if not service:
logger.error("Failed to build Cloud Identity service")
return
request = service.policies().list(
pageSize=100,
filter='setting.type.matches("chat.*")',
)
fetch_succeeded = True
while request is not None:
try:
response = request.execute()
for policy in response.get("policies", []):
if not self._is_customer_level_policy(policy):
continue
setting = policy.get("setting", {})
setting_type = setting.get("type", "").removeprefix("settings/")
logger.debug(f"Processing setting type: {setting_type}")
value = setting.get("value", {})
if setting_type == "chat.chat_file_sharing":
self.policies.external_file_sharing = value.get(
"externalFileSharing"
)
self.policies.internal_file_sharing = value.get(
"internalFileSharing"
)
logger.debug("Chat file sharing settings fetched.")
elif setting_type == "chat.external_chat_restriction":
self.policies.allow_external_chat = value.get(
"allowExternalChat"
)
self.policies.external_chat_restriction = value.get(
"externalChatRestriction"
)
logger.debug(
"Chat external chat restriction settings fetched."
)
elif setting_type == "chat.chat_external_spaces":
self.policies.external_spaces_enabled = value.get("enabled")
self.policies.external_spaces_domain_allowlist_mode = (
value.get("domainAllowlistMode")
)
logger.debug("Chat external spaces settings fetched.")
elif setting_type == "chat.chat_apps_access":
self.policies.enable_apps = value.get("enableApps")
self.policies.enable_webhooks = value.get("enableWebhooks")
logger.debug("Chat apps access settings fetched.")
request = service.policies().list_next(request, response)
except Exception as error:
self._handle_api_error(
error,
"fetching Chat policies",
self.provider.identity.customer_id,
)
fetch_succeeded = False
break
self.policies_fetched = fetch_succeeded
logger.info("Chat policies fetched successfully.")
except Exception as error:
self._handle_api_error(
error,
"fetching Chat policies",
self.provider.identity.customer_id,
)
self.policies_fetched = False
class ChatPolicies(BaseModel):
"""Model for domain-level Chat policy settings."""
# chat.chat_file_sharing
external_file_sharing: Optional[str] = None
internal_file_sharing: Optional[str] = None
# chat.external_chat_restriction
allow_external_chat: Optional[bool] = None
external_chat_restriction: Optional[str] = None
# chat.chat_external_spaces
external_spaces_enabled: Optional[bool] = None
external_spaces_domain_allowlist_mode: Optional[str] = None
# chat.chat_apps_access
enable_apps: Optional[bool] = None
enable_webhooks: Optional[bool] = None
@@ -24,6 +24,9 @@ class directory_super_admin_count(Check):
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=directory_client.provider.domain_resource,
resource_id="directoryUsers",
resource_name="Directory Users",
customer_id=directory_client.provider.identity.customer_id,
)
if 2 <= admin_count <= 4:
@@ -47,6 +47,9 @@ class directory_super_admin_only_admin_roles(Check):
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=directory_client.provider.domain_resource,
resource_id="directoryUsers",
resource_name="Directory Users",
customer_id=directory_client.provider.identity.customer_id,
)
report.status = "PASS"
report.status_extended = (
+208 -4
View File
@@ -22,7 +22,6 @@ dev = [
"pytest-env==1.1.5",
"pytest-randomly==3.16.0",
"pytest-xdist==3.6.1",
"safety==3.7.0",
"vulture==2.14"
]
@@ -151,7 +150,212 @@ AWS_SECURITY_TOKEN = 'testing'
AWS_SESSION_TOKEN = 'testing'
[tool.uv]
# cartography (pulled in via the API) still pins okta<1.0.0 for its (unused-by-prowler)
# intel.okta integration; the SDK Okta provider needs okta==3.4.2 (PR #11079). Force the
# version prowler needs; cartography's okta module is not imported here.
# Transitive pins matching the current lock to prevent silent drift on `uv lock`
# (e.g. supply chain hijacks via newer releases). Bump deliberately.
constraint-dependencies = [
"about-time==4.2.1",
"aenum==3.1.17",
"aiofiles==24.1.0",
"aiohappyeyeballs==2.6.1",
"aiohttp==3.13.5",
"aiosignal==1.4.0",
"alibabacloud-actiontrail20200706==2.4.1",
"alibabacloud-credentials==1.0.3",
"alibabacloud-credentials-api==1.0.0",
"alibabacloud-cs20151215==6.1.0",
"alibabacloud-darabonba-array==0.1.0",
"alibabacloud-darabonba-encode-util==0.0.2",
"alibabacloud-darabonba-map==0.0.1",
"alibabacloud-darabonba-signature-util==0.0.4",
"alibabacloud-darabonba-string==0.0.4",
"alibabacloud-darabonba-time==0.0.1",
"alibabacloud-ecs20140526==7.2.5",
"alibabacloud-endpoint-util==0.0.4",
"alibabacloud-gateway-oss==0.0.17",
"alibabacloud-gateway-sls==0.4.2",
"alibabacloud-gateway-sls-util==0.4.1",
"alibabacloud-gateway-spi==0.0.3",
"alibabacloud-openapi-util==0.2.4",
"alibabacloud-oss-util==0.0.6",
"alibabacloud-oss20190517==1.0.6",
"alibabacloud-ram20150501==1.2.0",
"alibabacloud-sas20181203==6.1.0",
"alibabacloud-sts20150401==1.1.6",
"alibabacloud-tea==0.4.3",
"alibabacloud-tea-openapi==0.4.4",
"alibabacloud-tea-util==0.3.14",
"alibabacloud-tea-xml==0.0.3",
"alibabacloud-vpc20160428==6.13.0",
"aliyun-log-fastpb==0.3.0",
"annotated-types==0.7.0",
"antlr4-python3-runtime==4.13.2",
"anyio==4.13.0",
"apscheduler==3.11.2",
"astroid==3.3.11",
"async-timeout==5.0.1",
"attrs==26.1.0",
"aws-sam-translator==1.109.0",
"aws-xray-sdk==2.15.0",
"azure-common==1.1.28",
"azure-core==1.41.0",
"azure-mgmt-core==1.6.0",
"bandit==1.8.3",
"black==25.1.0",
"blinker==1.9.0",
"certifi==2026.4.22",
"cffi==2.0.0",
"cfn-lint==1.51.0",
"charset-normalizer==3.4.7",
"circuitbreaker==2.1.3",
"click==8.3.3",
"click-plugins==1.1.1.2",
"contextlib2==21.6.0",
"coverage==7.6.12",
"darabonba-core==1.0.5",
"decorator==5.2.1",
"dill==0.4.1",
"distro==1.9.0",
"dnspython==2.8.0",
"docker==7.1.0",
"dogpile-cache==1.5.0",
"durationpy==0.10",
"email-validator==2.2.0",
"exceptiongroup==1.3.1",
"execnet==2.1.2",
"filelock==3.20.3",
"flake8==7.1.2",
"flask==3.1.3",
"freezegun==1.5.1",
"frozenlist==1.8.0",
"google-api-core==2.30.3",
"google-auth==2.52.0",
"googleapis-common-protos==1.75.0",
"graphemeu==0.7.2",
"graphql-core==3.2.8",
"h11==0.16.0",
"hpack==4.1.0",
"httpcore==1.0.9",
"httplib2==0.31.2",
"httpx==0.28.1",
"hyperframe==6.1.0",
"iamdata==0.1.202605131",
"idna==3.15",
"importlib-metadata==8.7.1",
"iniconfig==2.3.0",
"iso8601==2.1.0",
"isodate==0.7.2",
"isort==6.1.0",
"itsdangerous==2.2.0",
"jinja2==3.1.6",
"jmespath==1.1.0",
"joserfc==1.6.5",
"jsonpatch==1.33",
"jsonpath-ng==1.8.0",
"jsonpointer==3.1.1",
"jsonschema-path==0.3.4",
"jsonschema-specifications==2025.9.1",
"jwcrypto==1.5.7",
"keystoneauth1==5.14.0",
"lazy-object-proxy==1.12.0",
"lz4==4.4.5",
"markdown-it-py==4.2.0",
"markupsafe==3.0.3",
"mccabe==0.7.0",
"mdurl==0.1.2",
"microsoft-kiota-authentication-azure==1.9.2",
"microsoft-kiota-http==1.9.2",
"microsoft-kiota-serialization-form==1.9.2",
"microsoft-kiota-serialization-json==1.9.2",
"microsoft-kiota-serialization-multipart==1.9.2",
"microsoft-kiota-serialization-text==1.9.2",
"mock==5.2.0",
"moto==5.1.11",
"mpmath==1.3.0",
"msal==1.36.0",
"msal-extensions==1.3.1",
"msgraph-core==1.3.8",
"msrest==0.7.1",
"multidict==6.7.1",
"multipart==1.3.1",
"mypy-extensions==1.1.0",
"narwhals==2.21.0",
"nest-asyncio==1.6.0",
"networkx==3.4.2",
"oauthlib==3.3.1",
"openapi-schema-validator==0.6.3",
"openapi-spec-validator==0.7.1",
"opentelemetry-api==1.41.1",
"opentelemetry-sdk==1.41.1",
"opentelemetry-semantic-conventions==0.62b1",
"os-service-types==1.8.2",
"packaging==26.2",
"pathable==0.4.4",
"pathspec==1.1.1",
"pbr==7.0.3",
"platformdirs==4.9.6",
"plotly==6.7.0",
"pluggy==1.6.0",
"prek==0.3.9",
"propcache==0.5.2",
"proto-plus==1.28.0",
"protobuf==7.34.1",
"psutil==7.2.2",
"py-partiql-parser==0.6.1",
"pyasn1==0.6.3",
"pyasn1-modules==0.4.2",
"pycodestyle==2.12.1",
"pycparser==3.0",
"pycryptodomex==3.23.0",
"pydantic-core==2.41.5",
"pydash==8.0.6",
"pyflakes==3.2.0",
"pygments==2.20.0",
"pyjwt==2.12.1",
"pylint==3.3.4",
"pynacl==1.6.2",
"pyopenssl==26.2.0",
"pyparsing==3.3.2",
"pytest==8.3.5",
"pytest-cov==6.0.0",
"pytest-env==1.1.5",
"pytest-randomly==3.16.0",
"pytest-xdist==3.6.1",
"pywin32==311",
"pyyaml==6.0.3",
"referencing==0.36.2",
"regex==2026.5.9",
"requests==2.34.0",
"requests-file==3.0.1",
"requests-oauthlib==2.0.0",
"requestsexceptions==1.4.0",
"responses==0.26.0",
"retrying==1.4.2",
"rfc3339-validator==0.1.4",
"rich==15.0.0",
"rpds-py==0.30.0",
"s3transfer==0.14.0",
"setuptools==82.0.1",
"six==1.17.0",
"sniffio==1.3.1",
"std-uritemplate==2.0.8",
"stevedore==5.7.0",
"sympy==1.14.0",
"tldextract==5.3.1",
"tomli==2.4.1",
"tomlkit==0.15.0",
"typing-extensions==4.15.0",
"typing-inspection==0.4.2",
"tzdata==2026.2",
"uritemplate==4.2.0",
"urllib3==2.7.0",
"vulture==2.14",
"websocket-client==1.9.0",
"werkzeug==3.1.8",
"wrapt==2.1.2",
"xlsxwriter==3.2.9",
"xmltodict==1.0.4",
"yarl==1.23.0",
"zipp==3.23.1",
"zstd==1.5.7.3"
]
override-dependencies = ["okta==3.4.2"]
+2 -2
View File
@@ -32,7 +32,7 @@ fi
echo ""
# Full setup requires uv for system hooks (pylint, bandit, safety, vulture, trufflehog)
# Full setup requires uv for system hooks (pylint, bandit, vulture, trufflehog)
# These are installed as Python dev dependencies and used by local hooks in .pre-commit-config.yaml
if command -v uv &>/dev/null && [ -f "pyproject.toml" ]; then
if uv run prek --version &>/dev/null 2>&1; then
@@ -50,7 +50,7 @@ elif command -v prek &>/dev/null; then
prek install --overwrite
echo ""
echo -e "${YELLOW}⚠️ Warning: Some hooks require Python tools installed via uv:${NC}"
echo -e " pylint, bandit, safety, vulture, trufflehog"
echo -e " pylint, bandit, vulture, trufflehog"
echo -e " These hooks will be skipped unless you install them or run:"
echo -e " ${GREEN}uv sync${NC}"
else
@@ -0,0 +1,119 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatAppsInstallationDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_apps=False)
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == ChatPolicies(enable_apps=False).dict()
def test_fail_enabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_apps=True)
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "enabled" in findings[0].status_extended
def test_pass_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_apps=None)
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,149 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatExternalFileSharingDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing="NO_FILES")
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource
== ChatPolicies(external_file_sharing="NO_FILES").dict()
)
def test_fail_all_files(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing="ALL_FILES")
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "ALL_FILES" in findings[0].status_extended
def test_fail_images_only(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing="IMAGES_ONLY")
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "IMAGES_ONLY" in findings[0].status_extended
def test_fail_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing=None)
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not explicitly configured" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,154 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatExternalMessagingRestricted:
def test_pass_external_chat_disabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(allow_external_chat=False)
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource == ChatPolicies(allow_external_chat=False).dict()
)
def test_pass_trusted_domains(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
allow_external_chat=True,
external_chat_restriction="TRUSTED_DOMAINS",
)
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "restricted to allowed domains" in findings[0].status_extended
def test_fail_no_restriction(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
allow_external_chat=True,
external_chat_restriction="NO_RESTRICTION",
)
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not restricted" in findings[0].status_extended
def test_pass_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies()
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,155 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatExternalSpacesRestricted:
def test_pass_spaces_disabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_spaces_enabled=False)
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource
== ChatPolicies(external_spaces_enabled=False).dict()
)
def test_pass_trusted_domains(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
external_spaces_enabled=True,
external_spaces_domain_allowlist_mode="TRUSTED_DOMAINS",
)
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "restricted to allowed domains" in findings[0].status_extended
def test_fail_all_domains(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
external_spaces_enabled=True,
external_spaces_domain_allowlist_mode="ALL_DOMAINS",
)
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not restricted" in findings[0].status_extended
def test_fail_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies()
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not explicitly configured" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,119 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatIncomingWebhooksDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_webhooks=False)
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == ChatPolicies(enable_webhooks=False).dict()
def test_fail_enabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_webhooks=True)
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "enabled" in findings[0].status_extended
def test_pass_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_webhooks=None)
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,122 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatInternalFileSharingDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(internal_file_sharing="NO_FILES")
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource
== ChatPolicies(internal_file_sharing="NO_FILES").dict()
)
def test_fail_all_files(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(internal_file_sharing="ALL_FILES")
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "ALL_FILES" in findings[0].status_extended
def test_fail_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(internal_file_sharing=None)
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not explicitly configured" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,440 @@
from unittest.mock import MagicMock, patch
from googleapiclient.errors import HttpError
from httplib2 import Response as HttpResponse
from tests.providers.googleworkspace.googleworkspace_fixtures import (
ROOT_ORG_UNIT_ID,
set_mocked_googleworkspace_provider,
)
class TestChatService:
def test_chat_fetch_policies_all_settings(self):
"""Test fetching all 4 Chat policy settings from Cloud Identity API"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_credentials = MagicMock()
mock_session = MagicMock()
mock_session.credentials = mock_credentials
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {
"policies": [
{
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "NO_FILES",
"internalFileSharing": "IMAGES_ONLY",
},
}
},
{
"setting": {
"type": "settings/chat.external_chat_restriction",
"value": {
"allowExternalChat": True,
"externalChatRestriction": "TRUSTED_DOMAINS",
},
}
},
{
"setting": {
"type": "settings/chat.chat_external_spaces",
"value": {
"enabled": True,
"domainAllowlistMode": "TRUSTED_DOMAINS",
},
}
},
{
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {
"enableApps": False,
"enableWebhooks": False,
},
}
},
]
}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
assert chat.policies.external_file_sharing == "NO_FILES"
assert chat.policies.internal_file_sharing == "IMAGES_ONLY"
assert chat.policies.allow_external_chat is True
assert chat.policies.external_chat_restriction == "TRUSTED_DOMAINS"
assert chat.policies.external_spaces_enabled is True
assert (
chat.policies.external_spaces_domain_allowlist_mode == "TRUSTED_DOMAINS"
)
assert chat.policies.enable_apps is False
assert chat.policies.enable_webhooks is False
def test_chat_fetch_policies_empty_response(self):
"""Test handling empty policies response"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {"policies": []}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
assert chat.policies.external_file_sharing is None
assert chat.policies.allow_external_chat is None
assert chat.policies.enable_apps is None
assert chat.policies.enable_webhooks is None
def test_chat_fetch_policies_api_error(self):
"""Test handling of API errors during policy fetch"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_service.policies().list.side_effect = Exception("API Error")
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is False
assert chat.policies.external_file_sharing is None
def test_chat_fetch_policies_build_service_returns_none(self):
"""Test early return when _build_service fails to construct the client"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=None,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is False
assert chat.policies.external_file_sharing is None
def test_chat_fetch_policies_execute_raises(self):
"""Test inner except handler when request.execute() raises during pagination"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_request = MagicMock()
mock_request.execute.side_effect = Exception("Execute failed")
mock_service.policies().list.return_value = mock_request
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is False
assert chat.policies.external_file_sharing is None
def test_chat_fetch_policies_ignores_ou_and_group_level(self):
"""Test that OU-level and group-level policies are skipped, only customer-level used"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {
"policies": [
{
# Customer-level: no policyQuery → should be used
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": False, "enableWebhooks": False},
}
},
{
# OU-level: has policyQuery.orgUnit → should be skipped
"policyQuery": {"orgUnit": "orgUnits/sales_team"},
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": True, "enableWebhooks": True},
},
},
{
# Group-level: has policyQuery.group → should be skipped
"policyQuery": {"group": "groups/contractors"},
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "ALL_FILES",
"internalFileSharing": "ALL_FILES",
},
},
},
{
# Customer-level: no policyQuery → should be used
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "NO_FILES",
"internalFileSharing": "NO_FILES",
},
}
},
]
}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
assert chat.policies.enable_apps is False
assert chat.policies.external_file_sharing == "NO_FILES"
def test_chat_fetch_policies_accepts_root_ou(self):
"""Test that root-OU-scoped policies are accepted as customer-level"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {
"policies": [
{
# Root OU: matches provider's root_org_unit_id → should be accepted
"policyQuery": {"orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}"},
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": False, "enableWebhooks": True},
},
},
{
# Sub-OU: different orgUnit → should be skipped
"policyQuery": {"orgUnit": "orgUnits/sub_ou_sales"},
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "ALL_FILES",
"internalFileSharing": "ALL_FILES",
},
},
},
]
}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
# Root OU policy accepted
assert chat.policies.enable_apps is False
assert chat.policies.enable_webhooks is True
# Sub-OU policy skipped
assert chat.policies.external_file_sharing is None
def test_chat_partial_fetch_marks_policies_fetched_false(self):
"""Regression: if page 1 returns valid data but page 2 raises an error,
policies_fetched must be False even though some policy values were stored."""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
# Page 1: returns valid Chat data
page1_response = {
"policies": [
{
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": False, "enableWebhooks": False},
}
},
]
}
# Page 2 request raises HttpError 429
page1_request = MagicMock()
page1_request.execute.return_value = page1_response
page2_request = MagicMock()
page2_request.execute.side_effect = HttpError(
HttpResponse({"status": "429"}), b"Rate limit exceeded"
)
mock_service.policies().list.return_value = page1_request
mock_service.policies().list_next.return_value = page2_request
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
# Page 1 data was stored
assert chat.policies.enable_apps is False
# But policies_fetched must be False because page 2 failed
assert chat.policies_fetched is False
def test_chat_policies_model(self):
"""Test ChatPolicies Pydantic model"""
from prowler.providers.googleworkspace.services.chat.chat_service import (
ChatPolicies,
)
policies = ChatPolicies(
external_file_sharing="NO_FILES",
internal_file_sharing="IMAGES_ONLY",
allow_external_chat=True,
external_chat_restriction="TRUSTED_DOMAINS",
external_spaces_enabled=True,
external_spaces_domain_allowlist_mode="TRUSTED_DOMAINS",
enable_apps=False,
enable_webhooks=False,
)
assert policies.external_file_sharing == "NO_FILES"
assert policies.internal_file_sharing == "IMAGES_ONLY"
assert policies.allow_external_chat is True
assert policies.external_chat_restriction == "TRUSTED_DOMAINS"
assert policies.external_spaces_enabled is True
assert policies.external_spaces_domain_allowlist_mode == "TRUSTED_DOMAINS"
assert policies.enable_apps is False
assert policies.enable_webhooks is False
@@ -3,7 +3,6 @@ from unittest.mock import patch
from prowler.providers.googleworkspace.services.directory.directory_service import User
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
DOMAIN,
set_mocked_googleworkspace_provider,
)
@@ -54,8 +53,8 @@ class TestDirectorySuperAdminCount:
assert findings[0].status == "PASS"
assert "2 super administrator(s)" in findings[0].status_extended
assert "within the recommended range" in findings[0].status_extended
assert findings[0].resource_name == DOMAIN
assert findings[0].resource_id == CUSTOMER_ID
assert findings[0].resource_name == "Directory Users"
assert findings[0].resource_id == "directoryUsers"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == mock_provider.domain_resource.dict()
@@ -6,7 +6,6 @@ from prowler.providers.googleworkspace.services.directory.directory_service impo
)
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
DOMAIN,
set_mocked_googleworkspace_provider,
)
@@ -90,8 +89,8 @@ class TestDirectorySuperAdminOnlyAdminRoles:
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "used only for super admin activities" in findings[0].status_extended
assert findings[0].resource_name == DOMAIN
assert findings[0].resource_id == CUSTOMER_ID
assert findings[0].resource_name == "Directory Users"
assert findings[0].resource_id == "directoryUsers"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == mock_provider.domain_resource.dict()
+1 -1
View File
@@ -6,5 +6,5 @@
"useTabs": false,
"semi": true,
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"]
"plugins": ["prettier-plugin-packagejson", "prettier-plugin-tailwindcss"]
}
+5
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🚀 Added
- UI health endpoint at `GET /api/health` for Docker Compose liveness checks [(#11145)](https://github.com/prowler-cloud/prowler/pull/11145)
- AWS findings and resource details now expose a "View in AWS Console" link that opens the resource directly in the AWS Console via the universal `/go/view` ARN resolver. The per-provider external link is rendered by a new shared `ExternalResourceLink` component, which also covers the existing IaC repository link [(#9172)](https://github.com/prowler-cloud/prowler/pull/9172)
### 🔄 Changed
@@ -20,6 +21,10 @@ All notable changes to the **Prowler UI** are documented in this file.
- Mute Findings modal now enforces the 100-character limit on the rule name input with a live counter and inline error, matching the existing reason field behaviour [(#11158)](https://github.com/prowler-cloud/prowler/pull/11158)
### 🔐 Security
- UI npm dependencies updated to patched versions for Next.js, Vite, LangChain, XML parsing, lodash, and related transitive packages [(#11171)](https://github.com/prowler-cloud/prowler/pull/11171)
---
## [1.26.2] (Prowler 5.26.2)
+69
View File
@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { GET } from "./route";
interface HealthResponse {
status: "healthy";
service: "prowler-ui";
}
const parseHealthResponse = async (response: Response) =>
(await response.json()) as HealthResponse;
describe("GET /api/health", () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
it("should return a healthy response when the Next.js route handler responds", async () => {
// Given
const expectedBody: HealthResponse = {
status: "healthy",
service: "prowler-ui",
};
// When
const response = await GET();
const body = await parseHealthResponse(response);
// Then
expect(response.status).toBe(200);
expect(response.headers.get("Cache-Control")).toBe("no-store");
expect(body).toEqual(expectedBody);
});
it("should not call fetch while evaluating UI liveness", async () => {
// Given
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
// When
await GET();
// Then
expect(fetchMock).not.toHaveBeenCalled();
});
it("should not depend on external health URLs", async () => {
// Given
vi.stubEnv(
"PROWLER_API_HEALTH_URL",
"https://api.example.com/health/ready",
);
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
// When
const response = await GET();
const body = await parseHealthResponse(response);
// Then
expect(response.status).toBe(200);
expect(body).toEqual({
status: "healthy",
service: "prowler-ui",
});
expect(fetchMock).not.toHaveBeenCalled();
});
});
+14
View File
@@ -0,0 +1,14 @@
const healthResponse = {
status: "healthy",
service: "prowler-ui",
} as const;
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json(healthResponse, {
headers: {
"Cache-Control": "no-store",
},
});
}
+55 -47
View File
@@ -10,10 +10,10 @@
{
"section": "dependencies",
"name": "@aws-sdk/client-bedrock-runtime",
"from": "3.948.0",
"to": "3.988.0",
"from": "3.988.0",
"to": "3.1045.0",
"strategy": "installed",
"generatedAt": "2026-02-12T11:04:39.164Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
@@ -74,18 +74,18 @@
{
"section": "dependencies",
"name": "@langchain/aws",
"from": "0.1.15",
"to": "1.1.0",
"from": "1.1.0",
"to": "1.3.7",
"strategy": "installed",
"generatedAt": "2025-12-12T10:01:54.132Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
"name": "@langchain/core",
"from": "1.1.8",
"to": "1.1.15",
"from": "1.1.46",
"to": "1.1.45",
"strategy": "installed",
"generatedAt": "2026-01-15T08:46:46.283Z"
"generatedAt": "2026-05-14T10:22:47.378Z"
},
{
"section": "dependencies",
@@ -98,10 +98,10 @@
{
"section": "dependencies",
"name": "@langchain/openai",
"from": "0.6.16",
"to": "1.1.3",
"from": "1.1.3",
"to": "1.4.5",
"strategy": "installed",
"generatedAt": "2025-12-12T10:01:54.132Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
@@ -114,10 +114,10 @@
{
"section": "dependencies",
"name": "@next/third-parties",
"from": "16.1.6",
"to": "16.2.3",
"from": "16.2.3",
"to": "16.2.6",
"strategy": "installed",
"generatedAt": "2026-04-17T09:52:03.464Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
@@ -442,10 +442,10 @@
{
"section": "dependencies",
"name": "langchain",
"from": "1.1.5",
"to": "1.2.10",
"from": "1.2.10",
"to": "1.4.0",
"strategy": "installed",
"generatedAt": "2026-01-15T08:46:46.283Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
@@ -482,10 +482,10 @@
{
"section": "dependencies",
"name": "next",
"from": "16.1.6",
"to": "16.2.3",
"from": "16.2.3",
"to": "16.2.6",
"strategy": "installed",
"generatedAt": "2026-04-17T09:52:03.464Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
@@ -506,10 +506,10 @@
{
"section": "dependencies",
"name": "react",
"from": "19.2.4",
"to": "19.2.5",
"from": "19.2.5",
"to": "19.2.6",
"strategy": "installed",
"generatedAt": "2026-04-17T09:52:03.464Z"
"generatedAt": "2026-05-14T10:34:34.526Z"
},
{
"section": "dependencies",
@@ -522,10 +522,10 @@
{
"section": "dependencies",
"name": "react-dom",
"from": "19.2.4",
"to": "19.2.5",
"from": "19.2.5",
"to": "19.2.6",
"strategy": "installed",
"generatedAt": "2026-04-17T09:52:03.464Z"
"generatedAt": "2026-05-14T10:34:34.526Z"
},
{
"section": "dependencies",
@@ -635,9 +635,9 @@
"section": "dependencies",
"name": "zod",
"from": "4.1.11",
"to": "4.1.11",
"to": "4.4.3",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "dependencies",
@@ -658,10 +658,10 @@
{
"section": "devDependencies",
"name": "@next/eslint-plugin-next",
"from": "16.1.6",
"to": "16.2.3",
"from": "16.2.3",
"to": "16.2.6",
"strategy": "installed",
"generatedAt": "2026-04-17T09:52:03.464Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "devDependencies",
@@ -778,26 +778,26 @@
{
"section": "devDependencies",
"name": "@vitest/browser",
"from": "4.0.18",
"from": "4.1.6",
"to": "4.0.18",
"strategy": "installed",
"generatedAt": "2026-04-30T13:13:39.682Z"
"generatedAt": "2026-05-14T10:22:47.378Z"
},
{
"section": "devDependencies",
"name": "@vitest/browser-playwright",
"from": "4.0.18",
"from": "4.1.6",
"to": "4.0.18",
"strategy": "installed",
"generatedAt": "2026-04-30T13:13:39.682Z"
"generatedAt": "2026-05-14T10:22:47.378Z"
},
{
"section": "devDependencies",
"name": "@vitest/coverage-v8",
"from": "4.0.18",
"from": "4.1.6",
"to": "4.0.18",
"strategy": "installed",
"generatedAt": "2026-01-29T16:42:27.795Z"
"generatedAt": "2026-05-14T10:22:47.378Z"
},
{
"section": "devDependencies",
@@ -810,10 +810,10 @@
{
"section": "devDependencies",
"name": "dotenv",
"from": "16.6.1",
"to": null,
"from": null,
"to": "16.6.1",
"strategy": "installed",
"generatedAt": "2026-05-12T10:13:36.469Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "devDependencies",
@@ -931,9 +931,9 @@
"section": "devDependencies",
"name": "postcss",
"from": "8.4.38",
"to": "8.4.38",
"to": "8.5.14",
"strategy": "installed",
"generatedAt": "2026-05-11T15:15:31.276Z"
"generatedAt": "2026-05-14T10:09:04.901Z"
},
{
"section": "devDependencies",
@@ -943,6 +943,14 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "prettier-plugin-packagejson",
"from": "2.5.22",
"to": "2.5.22",
"strategy": "installed",
"generatedAt": "2026-05-13T14:59:45.730Z"
},
{
"section": "devDependencies",
"name": "prettier-plugin-tailwindcss",
@@ -970,17 +978,17 @@
{
"section": "devDependencies",
"name": "vitest",
"from": "4.0.18",
"from": "4.1.6",
"to": "4.0.18",
"strategy": "installed",
"generatedAt": "2026-01-29T16:42:27.795Z"
"generatedAt": "2026-05-14T10:22:47.378Z"
},
{
"section": "devDependencies",
"name": "vitest-browser-react",
"from": "2.0.4",
"from": "2.2.0",
"to": "2.0.4",
"strategy": "installed",
"generatedAt": "2026-04-30T13:23:20.132Z"
"generatedAt": "2026-05-14T10:22:47.378Z"
}
]
+42 -44
View File
@@ -1,40 +1,41 @@
{
"name": "prowler-next-app",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"audit": "pnpm audit --audit-level critical",
"audit:fix": "pnpm audit fix",
"audit:high": "pnpm audit --audit-level high",
"build": "next build",
"start": "next start",
"start:standalone": "node .next/standalone/server.js",
"deps:log": "node scripts/update-dependency-log.js",
"postinstall": "node scripts/postinstall.js",
"typecheck": "tsc",
"dev": "next dev",
"format:check": "./node_modules/.bin/prettier --check .",
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write .",
"healthcheck": "pnpm run typecheck && pnpm run lint:check",
"postinstall": "node scripts/postinstall.js",
"lint:check": "eslint . --max-warnings 40",
"lint:fix": "eslint . --fix --max-warnings 40",
"lint:knip": "knip --max-issues 494",
"lint:knip:fix": "knip --fix --max-issues 494",
"format:check": "./node_modules/.bin/prettier --check .",
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write .",
"start": "next start",
"start:standalone": "node .next/standalone/server.js",
"test": "vitest run",
"test:watch": "vitest",
"test:unit": "vitest run --project unit",
"test:browser": "vitest run --project browser",
"test:browser:watch": "vitest --project browser",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans",
"test:e2e:ui": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --ui",
"test:e2e:debug": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --debug",
"test:e2e:headed": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --headed",
"test:e2e:report": "playwright show-report",
"test:e2e:install": "playwright install",
"audit": "pnpm audit --audit-level critical",
"audit:high": "pnpm audit --audit-level high",
"audit:fix": "pnpm audit fix"
"test:e2e:report": "playwright show-report",
"test:e2e:ui": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --ui",
"test:unit": "vitest run --project unit",
"test:watch": "vitest",
"typecheck": "tsc"
},
"dependencies": {
"@ai-sdk/react": "2.0.111",
"@aws-sdk/client-bedrock-runtime": "3.988.0",
"@aws-sdk/client-bedrock-runtime": "3.1045.0",
"@codemirror/language": "6.12.2",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.40.0",
@@ -42,12 +43,12 @@
"@extractus/feed-extractor": "7.1.7",
"@heroui/react": "2.8.4",
"@hookform/resolvers": "5.2.2",
"@langchain/aws": "1.1.0",
"@langchain/core": "1.1.15",
"@langchain/aws": "1.3.7",
"@langchain/core": "1.1.45",
"@langchain/mcp-adapters": "1.1.3",
"@langchain/openai": "1.1.3",
"@langchain/openai": "1.4.5",
"@lezer/highlight": "1.2.3",
"@next/third-parties": "16.2.3",
"@next/third-parties": "16.2.6",
"@radix-ui/react-alert-dialog": "1.1.14",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
@@ -88,17 +89,17 @@
"import-in-the-middle": "2.0.0",
"js-yaml": "4.1.1",
"jwt-decode": "4.0.0",
"langchain": "1.2.10",
"langchain": "1.4.0",
"lucide-react": "0.543.0",
"marked": "15.0.12",
"modern-screenshot": "4.7.0",
"nanoid": "5.1.6",
"next": "16.2.3",
"next": "16.2.6",
"next-auth": "5.0.0-beta.30",
"next-themes": "0.2.1",
"react": "19.2.5",
"react": "19.2.6",
"react-day-picker": "9.13.0",
"react-dom": "19.2.5",
"react-dom": "19.2.6",
"react-hook-form": "7.62.0",
"react-markdown": "10.1.0",
"recharts": "2.15.4",
@@ -112,12 +113,12 @@
"use-stick-to-bottom": "1.1.1",
"vaul": "1.1.2",
"world-atlas": "2.0.2",
"zod": "4.1.11",
"zod": "4.4.3",
"zustand": "5.0.8"
},
"devDependencies": {
"@iconify/react": "5.2.1",
"@next/eslint-plugin-next": "16.2.3",
"@next/eslint-plugin-next": "16.2.6",
"@playwright/test": "1.56.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
@@ -151,40 +152,37 @@
"jsdom": "27.4.0",
"knip": "6.3.1",
"msw": "2.13.4",
"postcss": "8.4.38",
"postcss": "8.5.14",
"prettier": "3.6.2",
"prettier-plugin-packagejson": "2.5.22",
"prettier-plugin-tailwindcss": "0.6.14",
"tailwindcss": "4.1.18",
"typescript": "5.5.4",
"vitest": "4.0.18",
"vitest-browser-react": "2.0.4"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"pnpm": {
"overrides": {
"@react-types/shared": "3.26.0",
"@hono/node-server": "1.19.14",
"@internationalized/date": "3.10.0",
"@react-aria/ssr>react": "19.2.5",
"@react-aria/ssr>react-dom": "19.2.5",
"@react-aria/visually-hidden>react": "19.2.5",
"@react-aria/interactions>react": "19.2.5",
"lodash": "4.17.23",
"lodash-es": "4.17.23",
"hono": "4.12.4",
"@hono/node-server": "1.19.10",
"@isaacs/brace-expansion": "5.0.1",
"fast-xml-parser": "5.3.8",
"serialize-javascript": "7.0.5",
"rollup@>=4": "4.59.0",
"minimatch@<4": "3.1.4",
"@react-aria/interactions>react": "19.2.6",
"@react-aria/ssr>react": "19.2.6",
"@react-aria/ssr>react-dom": "19.2.6",
"@react-aria/visually-hidden>react": "19.2.6",
"fast-xml-parser": "5.8.0",
"hono": "4.12.18",
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"minimatch@>=9 <10": "9.0.7",
"minimatch@>=10": "10.2.3",
"ajv@<7": "6.14.0",
"ajv@>=8": "8.18.0",
"qs": "6.14.2"
"minimatch@<4": "3.1.4",
"qs": "6.14.2",
"rollup@>=4": "4.59.0",
"serialize-javascript": "7.0.5"
}
},
"version": "0.0.1",
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"msw": {
"workerDirectory": [
"public"
+3579 -4056
View File
File diff suppressed because it is too large Load Diff
Generated
+207 -225
View File
@@ -8,6 +8,212 @@ resolution-markers = [
]
[manifest]
constraints = [
{ name = "about-time", specifier = "==4.2.1" },
{ name = "aenum", specifier = "==3.1.17" },
{ name = "aiofiles", specifier = "==24.1.0" },
{ name = "aiohappyeyeballs", specifier = "==2.6.1" },
{ name = "aiohttp", specifier = "==3.13.5" },
{ name = "aiosignal", specifier = "==1.4.0" },
{ name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" },
{ name = "alibabacloud-credentials", specifier = "==1.0.3" },
{ name = "alibabacloud-credentials-api", specifier = "==1.0.0" },
{ name = "alibabacloud-cs20151215", specifier = "==6.1.0" },
{ name = "alibabacloud-darabonba-array", specifier = "==0.1.0" },
{ name = "alibabacloud-darabonba-encode-util", specifier = "==0.0.2" },
{ name = "alibabacloud-darabonba-map", specifier = "==0.0.1" },
{ name = "alibabacloud-darabonba-signature-util", specifier = "==0.0.4" },
{ name = "alibabacloud-darabonba-string", specifier = "==0.0.4" },
{ name = "alibabacloud-darabonba-time", specifier = "==0.0.1" },
{ name = "alibabacloud-ecs20140526", specifier = "==7.2.5" },
{ name = "alibabacloud-endpoint-util", specifier = "==0.0.4" },
{ name = "alibabacloud-gateway-oss", specifier = "==0.0.17" },
{ name = "alibabacloud-gateway-sls", specifier = "==0.4.2" },
{ name = "alibabacloud-gateway-sls-util", specifier = "==0.4.1" },
{ name = "alibabacloud-gateway-spi", specifier = "==0.0.3" },
{ name = "alibabacloud-openapi-util", specifier = "==0.2.4" },
{ name = "alibabacloud-oss-util", specifier = "==0.0.6" },
{ name = "alibabacloud-oss20190517", specifier = "==1.0.6" },
{ name = "alibabacloud-ram20150501", specifier = "==1.2.0" },
{ name = "alibabacloud-sas20181203", specifier = "==6.1.0" },
{ name = "alibabacloud-sts20150401", specifier = "==1.1.6" },
{ name = "alibabacloud-tea", specifier = "==0.4.3" },
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.4" },
{ name = "alibabacloud-tea-util", specifier = "==0.3.14" },
{ name = "alibabacloud-tea-xml", specifier = "==0.0.3" },
{ name = "alibabacloud-vpc20160428", specifier = "==6.13.0" },
{ name = "aliyun-log-fastpb", specifier = "==0.3.0" },
{ name = "annotated-types", specifier = "==0.7.0" },
{ name = "antlr4-python3-runtime", specifier = "==4.13.2" },
{ name = "anyio", specifier = "==4.13.0" },
{ name = "apscheduler", specifier = "==3.11.2" },
{ name = "astroid", specifier = "==3.3.11" },
{ name = "async-timeout", specifier = "==5.0.1" },
{ name = "attrs", specifier = "==26.1.0" },
{ name = "aws-sam-translator", specifier = "==1.109.0" },
{ name = "aws-xray-sdk", specifier = "==2.15.0" },
{ name = "azure-common", specifier = "==1.1.28" },
{ name = "azure-core", specifier = "==1.41.0" },
{ name = "azure-mgmt-core", specifier = "==1.6.0" },
{ name = "bandit", specifier = "==1.8.3" },
{ name = "black", specifier = "==25.1.0" },
{ name = "blinker", specifier = "==1.9.0" },
{ name = "certifi", specifier = "==2026.4.22" },
{ name = "cffi", specifier = "==2.0.0" },
{ name = "cfn-lint", specifier = "==1.51.0" },
{ name = "charset-normalizer", specifier = "==3.4.7" },
{ name = "circuitbreaker", specifier = "==2.1.3" },
{ name = "click", specifier = "==8.3.3" },
{ name = "click-plugins", specifier = "==1.1.1.2" },
{ name = "contextlib2", specifier = "==21.6.0" },
{ name = "coverage", specifier = "==7.6.12" },
{ name = "darabonba-core", specifier = "==1.0.5" },
{ name = "decorator", specifier = "==5.2.1" },
{ name = "dill", specifier = "==0.4.1" },
{ name = "distro", specifier = "==1.9.0" },
{ name = "dnspython", specifier = "==2.8.0" },
{ name = "docker", specifier = "==7.1.0" },
{ name = "dogpile-cache", specifier = "==1.5.0" },
{ name = "durationpy", specifier = "==0.10" },
{ name = "email-validator", specifier = "==2.2.0" },
{ name = "exceptiongroup", specifier = "==1.3.1" },
{ name = "execnet", specifier = "==2.1.2" },
{ name = "filelock", specifier = "==3.20.3" },
{ name = "flake8", specifier = "==7.1.2" },
{ name = "flask", specifier = "==3.1.3" },
{ name = "freezegun", specifier = "==1.5.1" },
{ name = "frozenlist", specifier = "==1.8.0" },
{ name = "google-api-core", specifier = "==2.30.3" },
{ name = "google-auth", specifier = "==2.52.0" },
{ name = "googleapis-common-protos", specifier = "==1.75.0" },
{ name = "graphemeu", specifier = "==0.7.2" },
{ name = "graphql-core", specifier = "==3.2.8" },
{ name = "h11", specifier = "==0.16.0" },
{ name = "hpack", specifier = "==4.1.0" },
{ name = "httpcore", specifier = "==1.0.9" },
{ name = "httplib2", specifier = "==0.31.2" },
{ name = "httpx", specifier = "==0.28.1" },
{ name = "hyperframe", specifier = "==6.1.0" },
{ name = "iamdata", specifier = "==0.1.202605131" },
{ name = "idna", specifier = "==3.15" },
{ name = "importlib-metadata", specifier = "==8.7.1" },
{ name = "iniconfig", specifier = "==2.3.0" },
{ name = "iso8601", specifier = "==2.1.0" },
{ name = "isodate", specifier = "==0.7.2" },
{ name = "isort", specifier = "==6.1.0" },
{ name = "itsdangerous", specifier = "==2.2.0" },
{ name = "jinja2", specifier = "==3.1.6" },
{ name = "jmespath", specifier = "==1.1.0" },
{ name = "joserfc", specifier = "==1.6.5" },
{ name = "jsonpatch", specifier = "==1.33" },
{ name = "jsonpath-ng", specifier = "==1.8.0" },
{ name = "jsonpointer", specifier = "==3.1.1" },
{ name = "jsonschema-path", specifier = "==0.3.4" },
{ name = "jsonschema-specifications", specifier = "==2025.9.1" },
{ name = "jwcrypto", specifier = "==1.5.7" },
{ name = "keystoneauth1", specifier = "==5.14.0" },
{ name = "lazy-object-proxy", specifier = "==1.12.0" },
{ name = "lz4", specifier = "==4.4.5" },
{ name = "markdown-it-py", specifier = "==4.2.0" },
{ name = "markupsafe", specifier = "==3.0.3" },
{ name = "mccabe", specifier = "==0.7.0" },
{ name = "mdurl", specifier = "==0.1.2" },
{ name = "microsoft-kiota-authentication-azure", specifier = "==1.9.2" },
{ name = "microsoft-kiota-http", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-form", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-json", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.2" },
{ name = "mock", specifier = "==5.2.0" },
{ name = "moto", specifier = "==5.1.11" },
{ name = "mpmath", specifier = "==1.3.0" },
{ name = "msal", specifier = "==1.36.0" },
{ name = "msal-extensions", specifier = "==1.3.1" },
{ name = "msgraph-core", specifier = "==1.3.8" },
{ name = "msrest", specifier = "==0.7.1" },
{ name = "multidict", specifier = "==6.7.1" },
{ name = "multipart", specifier = "==1.3.1" },
{ name = "mypy-extensions", specifier = "==1.1.0" },
{ name = "narwhals", specifier = "==2.21.0" },
{ name = "nest-asyncio", specifier = "==1.6.0" },
{ name = "networkx", specifier = "==3.4.2" },
{ name = "oauthlib", specifier = "==3.3.1" },
{ name = "openapi-schema-validator", specifier = "==0.6.3" },
{ name = "openapi-spec-validator", specifier = "==0.7.1" },
{ name = "opentelemetry-api", specifier = "==1.41.1" },
{ name = "opentelemetry-sdk", specifier = "==1.41.1" },
{ name = "opentelemetry-semantic-conventions", specifier = "==0.62b1" },
{ name = "os-service-types", specifier = "==1.8.2" },
{ name = "packaging", specifier = "==26.2" },
{ name = "pathable", specifier = "==0.4.4" },
{ name = "pathspec", specifier = "==1.1.1" },
{ name = "pbr", specifier = "==7.0.3" },
{ name = "platformdirs", specifier = "==4.9.6" },
{ name = "plotly", specifier = "==6.7.0" },
{ name = "pluggy", specifier = "==1.6.0" },
{ name = "prek", specifier = "==0.3.9" },
{ name = "propcache", specifier = "==0.5.2" },
{ name = "proto-plus", specifier = "==1.28.0" },
{ name = "protobuf", specifier = "==7.34.1" },
{ name = "psutil", specifier = "==7.2.2" },
{ name = "py-partiql-parser", specifier = "==0.6.1" },
{ name = "pyasn1", specifier = "==0.6.3" },
{ name = "pyasn1-modules", specifier = "==0.4.2" },
{ name = "pycodestyle", specifier = "==2.12.1" },
{ name = "pycparser", specifier = "==3.0" },
{ name = "pycryptodomex", specifier = "==3.23.0" },
{ name = "pydantic-core", specifier = "==2.41.5" },
{ name = "pydash", specifier = "==8.0.6" },
{ name = "pyflakes", specifier = "==3.2.0" },
{ name = "pygments", specifier = "==2.20.0" },
{ name = "pyjwt", specifier = "==2.12.1" },
{ name = "pylint", specifier = "==3.3.4" },
{ name = "pynacl", specifier = "==1.6.2" },
{ name = "pyopenssl", specifier = "==26.2.0" },
{ name = "pyparsing", specifier = "==3.3.2" },
{ name = "pytest", specifier = "==8.3.5" },
{ name = "pytest-cov", specifier = "==6.0.0" },
{ name = "pytest-env", specifier = "==1.1.5" },
{ name = "pytest-randomly", specifier = "==3.16.0" },
{ name = "pytest-xdist", specifier = "==3.6.1" },
{ name = "pywin32", specifier = "==311" },
{ name = "pyyaml", specifier = "==6.0.3" },
{ name = "referencing", specifier = "==0.36.2" },
{ name = "regex", specifier = "==2026.5.9" },
{ name = "requests", specifier = "==2.34.0" },
{ name = "requests-file", specifier = "==3.0.1" },
{ name = "requests-oauthlib", specifier = "==2.0.0" },
{ name = "requestsexceptions", specifier = "==1.4.0" },
{ name = "responses", specifier = "==0.26.0" },
{ name = "retrying", specifier = "==1.4.2" },
{ name = "rfc3339-validator", specifier = "==0.1.4" },
{ name = "rich", specifier = "==15.0.0" },
{ name = "rpds-py", specifier = "==0.30.0" },
{ name = "s3transfer", specifier = "==0.14.0" },
{ name = "setuptools", specifier = "==82.0.1" },
{ name = "six", specifier = "==1.17.0" },
{ name = "sniffio", specifier = "==1.3.1" },
{ name = "std-uritemplate", specifier = "==2.0.8" },
{ name = "stevedore", specifier = "==5.7.0" },
{ name = "sympy", specifier = "==1.14.0" },
{ name = "tldextract", specifier = "==5.3.1" },
{ name = "tomli", specifier = "==2.4.1" },
{ name = "tomlkit", specifier = "==0.15.0" },
{ name = "typing-extensions", specifier = "==4.15.0" },
{ name = "typing-inspection", specifier = "==0.4.2" },
{ name = "tzdata", specifier = "==2026.2" },
{ name = "uritemplate", specifier = "==4.2.0" },
{ name = "urllib3", specifier = "==2.7.0" },
{ name = "vulture", specifier = "==2.14" },
{ name = "websocket-client", specifier = "==1.9.0" },
{ name = "werkzeug", specifier = "==3.1.8" },
{ name = "wrapt", specifier = "==2.1.2" },
{ name = "xlsxwriter", specifier = "==3.2.9" },
{ name = "xmltodict", specifier = "==1.0.4" },
{ name = "yarl", specifier = "==1.23.0" },
{ name = "zipp", specifier = "==3.23.1" },
{ name = "zstd", specifier = "==1.5.7.3" },
]
overrides = [{ name = "okta", specifier = "==3.4.2" }]
[[package]]
@@ -521,15 +727,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/a9/5a83b6547f88514f16098c7f8418b1a9490a92e644481662e9ebc3a98923/aliyun_log_fastpb-0.3.0-cp37-abi3-win_amd64.whl", hash = "sha256:522b734e17eca7797235f297ff6a607137d2be92034778bb041ea061cd9cd9ba", size = 125709, upload-time = "2026-04-28T03:06:47.405Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -604,19 +801,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
name = "authlib"
version = "1.7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "joserfc" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" },
]
[[package]]
name = "aws-sam-translator"
version = "1.109.0"
@@ -1084,40 +1268,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/74/3c/3814aba90a63e84c7de0eb6fdf67bd1a9115ac5f99ec5b7a817a5d5278ec/azure_storage_blob-12.24.1-py3-none-any.whl", hash = "sha256:77fb823fdbac7f3c11f7d86a5892e2f85e161e8440a7489babe2195bf248f09e", size = 408432, upload-time = "2025-01-22T21:27:23.082Z" },
]
[[package]]
name = "backports-datetime-fromisoformat"
version = "2.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" },
{ url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" },
{ url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" },
{ url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" },
{ url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" },
{ url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" },
{ url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" },
{ url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" },
{ url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" },
{ url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" },
{ url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" },
{ url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" },
{ url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" },
{ url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" },
{ url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" },
{ url = "https://files.pythonhosted.org/packages/a2/0f/69bbdde2e1e57c09b5f01788804c50e68b29890aada999f2b1a40519def9/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147", size = 27630, upload-time = "2024-12-28T20:17:19.442Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1d/1c84a50c673c87518b1adfeafcfd149991ed1f7aedc45d6e5eac2f7d19d7/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4", size = 34707, upload-time = "2024-12-28T20:17:21.79Z" },
{ url = "https://files.pythonhosted.org/packages/71/44/27eae384e7e045cda83f70b551d04b4a0b294f9822d32dea1cbf1592de59/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35", size = 27280, upload-time = "2024-12-28T20:17:24.503Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7a/a4075187eb6bbb1ff6beb7229db5f66d1070e6968abeb61e056fa51afa5e/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1", size = 55094, upload-time = "2024-12-28T20:17:25.546Z" },
{ url = "https://files.pythonhosted.org/packages/71/03/3fced4230c10af14aacadc195fe58e2ced91d011217b450c2e16a09a98c8/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32", size = 55605, upload-time = "2024-12-28T20:17:29.208Z" },
{ url = "https://files.pythonhosted.org/packages/f6/0a/4b34a838c57bd16d3e5861ab963845e73a1041034651f7459e9935289cfd/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d", size = 55353, upload-time = "2024-12-28T20:17:32.433Z" },
{ url = "https://files.pythonhosted.org/packages/d9/68/07d13c6e98e1cad85606a876367ede2de46af859833a1da12c413c201d78/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7", size = 55298, upload-time = "2024-12-28T20:17:34.919Z" },
{ url = "https://files.pythonhosted.org/packages/60/33/45b4d5311f42360f9b900dea53ab2bb20a3d61d7f9b7c37ddfcb3962f86f/backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d", size = 29375, upload-time = "2024-12-28T20:17:36.018Z" },
{ url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" },
{ url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" },
]
[[package]]
name = "bandit"
version = "1.8.3"
@@ -1264,8 +1414,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aws-sam-translator" },
{ name = "jsonpatch" },
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "networkx" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "sympy" },
@@ -1623,19 +1772,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/80/12235e5b75bb2c586733280854f131b86051e0bbdfb55349ff70d0f72cf9/dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d", size = 64447, upload-time = "2025-10-11T17:35:38.573Z" },
]
[[package]]
name = "dparse"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" },
]
[[package]]
name = "dulwich"
version = "0.23.0"
@@ -2084,15 +2220,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
[[package]]
name = "joblib"
version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "joserfc"
version = "1.6.5"
@@ -2350,19 +2477,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
]
[[package]]
name = "marshmallow"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-datetime-fromisoformat", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" },
]
[[package]]
name = "mccabe"
version = "0.7.0"
@@ -2710,42 +2824,11 @@ wheels = [
name = "networkx"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
]
[[package]]
name = "networkx"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "nltk"
version = "3.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
]
[[package]]
name = "numpy"
version = "2.0.2"
@@ -3260,7 +3343,6 @@ dev = [
{ name = "pytest-env" },
{ name = "pytest-randomly" },
{ name = "pytest-xdist" },
{ name = "safety" },
{ name = "vulture" },
]
@@ -3365,7 +3447,6 @@ dev = [
{ name = "pytest-env", specifier = "==1.1.5" },
{ name = "pytest-randomly", specifier = "==3.16.0" },
{ name = "pytest-xdist", specifier = "==3.6.1" },
{ name = "safety", specifier = "==3.7.0" },
{ name = "vulture", specifier = "==2.14" },
]
@@ -4074,15 +4155,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
]
[[package]]
name = "s3transfer"
version = "0.14.0"
@@ -4095,51 +4167,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
]
[[package]]
name = "safety"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
{ name = "click" },
{ name = "dparse" },
{ name = "filelock" },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "marshmallow" },
{ name = "nltk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "ruamel-yaml" },
{ name = "safety-schemas" },
{ name = "tenacity" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "tomlkit" },
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" },
]
[[package]]
name = "safety-schemas"
version = "0.0.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dparse" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "ruamel-yaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" },
]
[[package]]
name = "schema"
version = "0.7.5"
@@ -4161,15 +4188,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "shodan"
version = "1.31.0"
@@ -4250,15 +4268,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
]
[[package]]
name = "tenacity"
version = "9.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
[[package]]
name = "tldextract"
version = "5.3.1"
@@ -4310,33 +4319,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" },
]
[[package]]
name = "tqdm"
version = "4.67.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
[[package]]
name = "typer"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"