From f97c26e1263ef09c75a15a71b6b0b590ae18dfc5 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Thu, 16 Jul 2026 13:06:14 +0100 Subject: [PATCH 1/2] docs: prepare OpenSSF Silver readiness evidence --- .github/SECURITY.md | 9 ++- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 9 ++- GOVERNANCE.md | 62 +++++++++++++++ MAINTAINERS.md | 25 ++++++ README.md | 17 +++- ROADMAP.md | 43 ++++++++++ SECURITY_ACKNOWLEDGEMENTS.md | 9 +++ SUPPORT.md | 30 +++++++ docs/accessibility-and-i18n.md | 13 +++ docs/architecture.md | 12 +-- docs/ci-pipeline.md | 8 +- docs/learn/index.html | 4 +- docs/openssf-silver-evidence.md | 83 ++++++++++++++++++++ docs/regression-test-audit.md | 33 ++++++++ docs/release-security.md | 37 +++++++++ docs/rules-reference.md | 3 +- docs/security-assurance-case.md | 78 ++++++++++++++++++ docs/security-requirements.md | 49 ++++++++++++ frontend/package.json | 2 +- frontend/src/components/ai/ChatPanel.jsx | 2 +- frontend/src/components/layout/Header.jsx | 2 +- frontend/src/pages/AILayer.jsx | 6 +- frontend/src/pages/DetailedScan.jsx | 4 +- tests/test_azure_client_management.py | 96 +++++++++++++++++++++++ website/content.js | 16 +++- 26 files changed, 622 insertions(+), 32 deletions(-) create mode 100644 GOVERNANCE.md create mode 100644 MAINTAINERS.md create mode 100644 ROADMAP.md create mode 100644 SECURITY_ACKNOWLEDGEMENTS.md create mode 100644 SUPPORT.md create mode 100644 docs/accessibility-and-i18n.md create mode 100644 docs/openssf-silver-evidence.md create mode 100644 docs/regression-test-audit.md create mode 100644 docs/release-security.md create mode 100644 docs/security-assurance-case.md create mode 100644 docs/security-requirements.md create mode 100644 tests/test_azure_client_management.py diff --git a/.github/SECURITY.md b/.github/SECURITY.md index e8e98d2..315eebd 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -26,9 +26,10 @@ The more detail you provide, the faster we can respond. | Version | Supported | |---------|-----------| -| 0.1.x | Yes | +| 0.3.x | Yes | +| 0.1.x | No | -Older versions are not patched. If you are running a version below 0.1.x, upgrade to the latest release before filing a report. +Older versions are not patched unless a GitHub Security Advisory explicitly says otherwise. Upgrade to the latest release before filing a report. --- @@ -71,7 +72,7 @@ We ask that you do not publicly disclose the vulnerability until step 6 is compl We value responsible disclosure. Researchers who report valid vulnerabilities will be: - Acknowledged by name (or pseudonym if preferred) in the release notes for the fix -- Listed in a `SECURITY_ACKNOWLEDGEMENTS.md` file we maintain in this repository +- Listed in [`SECURITY_ACKNOWLEDGEMENTS.md`](../SECURITY_ACKNOWLEDGEMENTS.md) We do not currently offer a bug bounty programme, but we are grateful for every report. @@ -79,4 +80,4 @@ We do not currently offer a bug bounty programme, but we are grateful for every ## Contact -**Email: vishnu.ajith@owasp.org** \ No newline at end of file +**Email: vishnu.ajith@owasp.org** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ddbb30..ac6f5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -531,7 +531,7 @@ jobs: env: DATABASE_URL: "postgresql://ci:ci@localhost:5432/ci_db" run: | - pytest tests/ -v --tb=short --cov=api --cov=scanner --cov-report=term --cov-report=xml --cov-fail-under=25 + pytest tests/ -v --tb=short --cov=api --cov=scanner --cov-report=term --cov-report=xml --cov-fail-under=80 # ── Frontend lint + build ──────────────────────────────────────────────── frontend: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c26b8e8..5fcedd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -218,7 +218,7 @@ pip install -r requirements.txt # Installs Flask, Alembic, Azure SDK clients, requests, psycopg2, PyJWT, and PyYAML for CI workflow validation. # Frontend -# The frontend directory is currently a scaffold. The React dashboard MVP is on the roadmap. +# The React dashboard lives in frontend/ and uses the repository's npm scripts. # Database (Docker) docker run --name openshield-db \ @@ -268,6 +268,12 @@ All contributions must meet these standards before a pull request will be review - Follow Conventional Commits using prefixes such as `feat:`, `fix:`, `docs:`, `chore:`, and `test:`. - Reference the related issue number where applicable. +**Developer Certificate of Origin** + +- By adding `Signed-off-by: Your Name ` to a commit, a contributor certifies the [Developer Certificate of Origin 1.1](https://developercertificate.org/). +- Use `git commit -s` to add the sign-off. +- The project lead must approve and enable DCO enforcement before this becomes a required merge check; until then, sign-off is requested but not represented as enforced. + **Branch naming** - Use `feat/description` for new features. @@ -285,6 +291,7 @@ All contributions must meet these standards before a pull request will be review - Add a CLI remediation playbook for each new scanner rule. - Follow `.github/PULL_REQUEST_TEMPLATE.md`. - Obtain at least one reviewer approval before merge. +- Add regression tests for bug fixes whenever the behavior can be reproduced automatically. Major functionality must include automated tests. ## OpenSSF Best Practices diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..f27446d --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,62 @@ +# OpenShield Governance + +OpenShield uses a maintainer-led, consensus-seeking governance model. Technical +discussion happens in public GitHub issues and pull requests. The maintainers +seek agreement through review; when consensus cannot be reached, the project +lead makes the final decision and records the reasoning publicly. + +## Roles + +- **Project lead:** sets project direction, appoints maintainers, manages + releases and resolves decisions that cannot reach consensus. +- **Maintainers:** triage issues, review changes, uphold security and quality + requirements, and merge approved pull requests in their assigned areas. +- **Security maintainers:** privately triage vulnerability reports, coordinate + fixes and advisories, and ensure reporter credit is handled according to the + security policy. +- **Contributors:** propose issues and changes, participate in review, add tests + and documentation, and follow the Code of Conduct and contribution policy. + +Current role holders and component responsibilities are listed in +[`MAINTAINERS.md`](MAINTAINERS.md). Repository paths also have review owners in +`.github/CODEOWNERS`. + +## Decision and change process + +1. Material changes begin with a GitHub issue describing the problem, scope and + acceptance criteria. +2. Implementation is submitted by pull request to `dev` and must pass required + automated checks. +3. At least one qualified reviewer must approve before merge. Authors do not + approve their own changes. +4. Releases are promoted from `dev` to `main` and published by an authorized + maintainer. +5. Security-sensitive decisions may be discussed privately until coordinated + disclosure, after which the advisory and fix are made public. + +## Appointing and removing maintainers + +Regular contributors may be nominated as maintainers based on sustained, +constructive work and demonstrated knowledge of the relevant component. The +project lead confirms appointments after consulting existing maintainers. +Maintainers may step down at any time. Access may be removed for inactivity, +security risk, repeated policy violations, or Code of Conduct violations. + +## Continuity + +The project intends to maintain at least two people capable of issue triage, +pull-request review, merging and release operations. Administrative access, +deployment access, domains and recovery methods must be held through +organization-controlled accounts or recoverable records rather than a single +person's undocumented credentials. + +The project lead reviews continuity access before each release. The names of +people who currently hold each capability are intentionally confirmed through +private organization records; `MAINTAINERS.md` lists the public accountable +roles without publishing secret locations or recovery details. + +## Governance changes + +Governance changes use the normal issue and pull-request process and require +approval from the project lead and one additional maintainer. Emergency +security changes may be merged first and documented immediately afterward. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..40f3eed --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,25 @@ +# OpenShield Maintainers + +This file identifies public project roles and responsibilities. It does not +assert organization permissions that have not been confirmed by the project +lead. + +| Role | Account | Responsibilities | +|---|---|---| +| Project lead and release owner | [@Vishnu2707](https://github.com/Vishnu2707) | Direction, final governance decisions, releases and organization administration | +| Scanner and playbook maintainer | [@TFT444](https://github.com/TFT444) | Azure scanner rules, remediation playbooks, tests and review | +| Scanner and compliance maintainer | [@SHAURYAKSHARMA24](https://github.com/SHAURYAKSHARMA24) | Scanner rules, playbooks and compliance mappings | +| API and AI maintainer | [@ritiksah141](https://github.com/ritiksah141) | API, AI layer, CI/infra review and backend tests | +| Frontend maintainer | [@vogonPrayas](https://github.com/vogonPrayas) | Dashboard implementation and frontend review | +| Test maintainer | [@parthrohit22](https://github.com/parthrohit22) | Test suite and documentation review | + +Path-specific review assignments are maintained in `.github/CODEOWNERS`. +Private security reports are coordinated using `.github/SECURITY.md`. + +## Owner confirmation required + +Before OpenShield claims the OpenSSF continuity criteria, the project lead must +confirm that at least two people can perform issue administration, merge +approved changes and publish a release if any one maintainer becomes +unavailable. Public role assignment alone is not proof of administrative +access. diff --git a/README.md b/README.md index e3f43a5..0d9c900 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ Findings map to NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA | Feature | Description | |---|---| -| **Misconfiguration Scanner** | Runs 39 Azure security rules across storage, network, identity, database, compute, Key Vault, and post-quantum cryptography | +| **Misconfiguration Scanner** | Runs 51 Azure security rules across storage, network, identity, database, compute, Key Vault, AKS, and post-quantum cryptography | | **Compliance Mapper** | Maps findings to CIS Benchmarks, NIST CSF, ISO 27001, and SOC 2 framework JSON files | | **Scan History API** | Stores scans and findings in PostgreSQL and exposes findings, score, scan history, compliance posture, drift, and resource inventory over REST | -| **Remediation Playbooks** | Every rule ships with a matching Azure CLI remediation script (36 playbooks) | +| **Remediation Playbooks** | Every rule ships with a matching Azure CLI remediation script (51 playbooks) | | **Security Dashboard** | Full React dashboard deployed on Vercel - live monitoring, findings, compliance, drift, prioritization, and AI-layer views | | **Project Website** | Documentation and reference site at [openshield-website.vercel.app](https://openshield-website.vercel.app) - blog, rules gallery, docs, roadmap, releases, and interactive playground | | **Sentinel Integration** | Normalises findings and pushes them into Microsoft Sentinel via a Log Analytics custom table and KQL analytics rules | @@ -76,6 +76,15 @@ The project's OpenSSF status is publicly verifiable through the official OpenSSF **[View OpenShield's verified OpenSSF Best Practices record](https://www.bestpractices.dev/projects/13618)** +Project policies and assurance evidence: + +- [Governance](GOVERNANCE.md) and [maintainer responsibilities](MAINTAINERS.md) +- [July 2026–June 2027 roadmap](ROADMAP.md) +- [Support and upgrade policy](SUPPORT.md) +- [Security requirements](docs/security-requirements.md) and [security assurance case](docs/security-assurance-case.md) +- [Release security](docs/release-security.md) and [accessibility/i18n policy](docs/accessibility-and-i18n.md) +- [OpenSSF Silver evidence register](docs/openssf-silver-evidence.md) + --- ## Architecture @@ -84,11 +93,11 @@ The project's OpenSSF status is publicly verifiable through the official OpenSSF flowchart TD A["React Dashboard\nVercel · Live"] B["Flask REST API\nJWT · CORS · Blueprints"] - C["Scanner Engine\n39 Python rules"] + C["Scanner Engine\n51 Python rules"] D["Azure Subscription\nScanned via Azure SDK + Graph"] E["Compliance Framework JSON\nCIS · NIST · ISO 27001 · SOC 2"] F["PostgreSQL Database\nFindings · Scans"] - G["Azure CLI Playbooks\n39 remediation scripts"] + G["Azure CLI Playbooks\n51 remediation scripts"] H["sentinel/ingest.py\nNormalise + HMAC upload"] I["Microsoft Sentinel\nOpenShieldFindings_CL · KQL rules"] diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..3a6ce1b --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,43 @@ +# OpenShield Roadmap: July 2026 to June 2027 + +This roadmap communicates direction rather than a delivery guarantee. Security +fixes and community needs may change priorities. Work is accepted only through +reviewed issues and pull requests. + +## July–September 2026 + +- Complete OpenSSF Best Practices Silver readiness and evidence. +- Expand enterprise Azure coverage while preserving low false-positive rates. +- Improve scanner inventory error handling and rule test coverage. +- Reconcile documentation, website rule counts and current APIs. + +## October–December 2026 + +- Stabilize the scanner and REST API contracts for a supported 1.0 release. +- Improve deployment, upgrade and database migration guidance. +- Expand Sentinel detections and operational security monitoring. +- Establish signed release artifacts and public verification instructions. + +## January–March 2027 + +- Add organization-scale scanning and clearer multi-subscription workflows. +- Improve accessibility and internationalization readiness in user interfaces. +- Expand evidence-based compliance reporting and exception handling. +- Evaluate a provider interface for future clouds without weakening Azure + support. + +## April–June 2027 + +- Review reliability, performance and recovery objectives using production + feedback. +- Mature compatibility, deprecation and long-term maintenance policies. +- Reassess readiness for OpenSSF Gold without claiming it prematurely. + +## Explicitly out of scope for this period + +- Automatic remediation without explicit operator confirmation. +- Claims of formal certification for CIS, ISO 27001, SOC 2 or NIST. +- Collection of Azure resource contents, secrets or customer workload data. +- Guaranteed detection of every cloud vulnerability or configuration risk. +- Multi-cloud parity until the Azure implementation and provider boundary are + stable. diff --git a/SECURITY_ACKNOWLEDGEMENTS.md b/SECURITY_ACKNOWLEDGEMENTS.md new file mode 100644 index 0000000..97c2bae --- /dev/null +++ b/SECURITY_ACKNOWLEDGEMENTS.md @@ -0,0 +1,9 @@ +# Security Acknowledgements + +OpenShield credits reporters of resolved vulnerabilities unless they request +anonymity. Credits are added here and to the relevant security advisory or +release notes after coordinated disclosure. + +No externally reported vulnerability has been publicly disclosed by the project +as of 16 July 2026. This statement should be updated whenever a coordinated +disclosure is completed. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..8a716b8 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,30 @@ +# Support and Version Policy + +## Supported releases + +OpenShield supports the latest published minor release. Security fixes may also +be issued for the immediately preceding minor release when an upgrade cannot be +completed promptly. Older releases are unsupported unless a GitHub Security +Advisory explicitly states otherwise. + +The authoritative release list is the project's GitHub Releases page. Users +should include the release tag or commit SHA when reporting a defect. + +## Upgrading + +1. Read `CHANGELOG.md` and the target GitHub Release notes. +2. Back up the database and deployment configuration. +3. Install the target source and its pinned dependency manifests. +4. Run `alembic upgrade head` before starting the API. +5. Validate `/health`, authentication, scanning and dashboard connectivity in a + non-production environment before promotion. + +Database-specific migration instructions are in +`docs/database-migrations.md`. Breaking changes and required operator actions +must be identified in release notes. + +## Security support + +Do not report vulnerabilities through a public issue. Follow +`.github/SECURITY.md` for the private reporting and coordinated disclosure +process. diff --git a/docs/accessibility-and-i18n.md b/docs/accessibility-and-i18n.md new file mode 100644 index 0000000..4665859 --- /dev/null +++ b/docs/accessibility-and-i18n.md @@ -0,0 +1,13 @@ +# Accessibility and Internationalization + +OpenShield aims for keyboard-operable interfaces, visible focus, semantic HTML, +text alternatives for meaningful images, labeled controls, adequate contrast, +responsive layouts and reduced-motion compatibility where practical. New UI +changes should be reviewed against WCAG 2.2 AA guidance and tested with keyboard +navigation. Accessibility defects are tracked like other product defects. + +English is currently the only supported interface language. User-facing text +should be kept separate from security data and logic where practical so a +future localization system can replace it without changing scanner behavior. +OpenShield must not claim full internationalization until the frontend and +website use message catalogs, locale-aware formatting and localization tests. diff --git a/docs/architecture.md b/docs/architecture.md index a920329..2249851 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,7 +2,7 @@ ## Overview -OpenShield is a modular, open source Cloud Security Posture Management (CSPM) platform for Azure. It scans your Azure subscription against 36 security rules, maps findings to compliance frameworks (CIS, NIST CSF, ISO 27001, SOC 2), stores results in PostgreSQL, and exposes posture data through a Flask REST API consumed by a live React dashboard. +OpenShield is a modular, open source Cloud Security Posture Management (CSPM) platform for Azure. It scans your Azure subscription against 51 security rules, maps findings to compliance frameworks (CIS, NIST CSF, ISO 27001, SOC 2), stores results in PostgreSQL, and exposes posture data through a Flask REST API consumed by a live React dashboard. --- @@ -43,8 +43,8 @@ OpenShield is a modular, open source Cloud Security Posture Management (CSPM) pl ┌───────────▼──────────────────────────────────────────────────────┐ │ Rule Modules (scanner/rules/) │ │ │ -│ 20 rule files across Storage, Network, Identity, Database, │ -│ Compute, and Key Vault │ +│ 51 rule files across Storage, Network, Identity, Database, │ +│ Compute, Key Vault, AKS, and post-quantum cryptography │ └───────────┬───────────────────────────────────────────────────────┘ │ calls ┌───────────▼──────────────────────────────────────────────────────┐ @@ -110,16 +110,18 @@ result = engine.run_scan() ### 4. Current Rule Modules -There are 36 rule files in `scanner/rules/`. See `docs/rules-reference.md` for the full table. +There are 51 rule files in `scanner/rules/`. See `docs/rules-reference.md` for the full table. | Category | Count | Rules | |---|---|---| | Storage | 5 | AZ-STOR-001 to 005 | -| Network | 14 | AZ-NET-001 to 014 | +| Network | 15 | AZ-NET-001 to 015 | | Identity | 4 | AZ-IDN-001 to 004 | | Database | 4 | AZ-DB-001 to 004 | | Compute | 4 | AZ-CMP-001 to 004 | | Key Vault | 5 | AZ-KV-001 to 005 | +| Kubernetes | 6 | AZ-AKS-001 to 006 | +| Post-quantum | 3 | AZ-PQC-001 to 003 | Every rule has a matching Azure CLI playbook in `playbooks/cli/`. diff --git a/docs/ci-pipeline.md b/docs/ci-pipeline.md index ba11313..e813032 100644 --- a/docs/ci-pipeline.md +++ b/docs/ci-pipeline.md @@ -19,7 +19,7 @@ This document explains each job, how to reproduce every check locally before ope | **SCA (pip-audit)** | `pip-audit -r requirements.txt` | A dependency with a known CVE (minus documented ignores) | | **SBOM (Syft)** | CycloneDX SBOM generated + uploaded as an artifact | SBOM generation error | | **Container Scan (Trivy)** | Dormant scaffold — skips until a `Dockerfile` exists (INFRA 1 / #154) | (nothing today) | -| **Backend Tests (pytest + coverage)** | Full `tests/` suite against an ephemeral Postgres, `--cov-fail-under=25` | A failing test or coverage below the floor | +| **Backend Tests (pytest + coverage)** | Full `tests/` suite against an ephemeral Postgres, `--cov-fail-under=80` | A failing test or coverage below the Silver-level floor | | **Frontend (lint + build)** | `npm ci` → `eslint` → `vite build` | An eslint error or a broken dashboard build | | **Enforce dev to main source** | `main` PRs must come from `dev` | A non-`dev` branch opening a PR into `main` | | **CI Summary** | Aggregates all job results into the run summary and fails if any required job failed | Any required job failing | @@ -109,10 +109,10 @@ The three ignores are advisories in `transformers` (a transitive dependency of ` ```bash DATABASE_URL="postgresql://ci:ci@localhost/ci_db" OPENSHIELD_ENV="testing" \ - pytest tests/ -v --tb=short --cov=api --cov=scanner --cov-report=term --cov-fail-under=25 + pytest tests/ -v --tb=short --cov=api --cov=scanner --cov-report=term --cov-fail-under=80 ``` -Runs the **entire** `tests/` suite once (not just rule tests). Tests requiring a vector store or an AI API key skip cleanly. `--cov-fail-under=25` is a floor to prevent backsliding; raise it as coverage grows. +Runs the **entire** `tests/` suite once (not just rule tests). Tests requiring a vector store or an AI API key skip cleanly. `--cov-fail-under=80` enforces the OpenSSF Silver statement-coverage requirement for the measured API and scanner packages. ### Frontend (lint + build) @@ -181,7 +181,7 @@ The `ci-summary` job uses `needs: [...]` + `if: always()` so it runs after every | `ruff check` / `format` fails | `ruff format .` then `ruff check --fix .`; re-run both | | `bandit` medium+ finding | Fix it, or if a confirmed false positive add `# nosec ` with a one-line reason | | `pip-audit` reports a CVE | Bump the pin to a fixed version; only add `--ignore-vuln` with a documented reason | -| `pytest` coverage below 25% | Add tests, or investigate the regression that removed coverage | +| `pytest` coverage below 80% | Add tests, or investigate the regression that removed coverage | | Frontend eslint error | Fix the reported rule (e.g. remove an unused import); warnings do not fail CI | | `Enforce dev to main source` fails | Open the PR from `dev`; merge feature work into `dev` first | | `missing field 'RULE_ID'` | Add `RULE_ID = "AZ-XXX-000"` at module level in the rule file | diff --git a/docs/learn/index.html b/docs/learn/index.html index 7ca6219..a9aaa4f 100644 --- a/docs/learn/index.html +++ b/docs/learn/index.html @@ -841,7 +841,7 @@

Production-shaped, MVP-friendly architecture

Rule coverage

-

39 Azure security rules

+

51 Azure security rules

OpenShield currently has 39 dynamic rules. The strongest contributor work improves rule accuracy, reduces false positives, strengthens validation, or improves remediation quality. @@ -931,7 +931,7 @@

Current cleanup items

Documentation drift

    -
  • Some README/docs references still mention 20 rules while the repo has 39.
  • +
  • Rule coverage and documentation counts are checked and updated with each release.
  • Some startup commands assume python, but local environments may only expose python3.
  • API docs and implementation should stay aligned, especially score response shape.
diff --git a/docs/openssf-silver-evidence.md b/docs/openssf-silver-evidence.md new file mode 100644 index 0000000..921d1c5 --- /dev/null +++ b/docs/openssf-silver-evidence.md @@ -0,0 +1,83 @@ +# OpenSSF Silver Evidence Register + +This register supports issue #199 and the official OpenSSF assessment. It is an +audit aid, not a badge claim. The BadgeApp owner must review every answer and +submit the public URL or justification. + +## Ready for evidence review + +| Criterion | Public evidence | +|---|---| +| `achieve_passing` | README badge and official project record | +| `contribution_requirements` | `CONTRIBUTING.md` | +| `governance` | `GOVERNANCE.md` | +| `code_of_conduct` | `.github/CODE_OF_CONDUCT.md` | +| `roles_responsibilities` | `GOVERNANCE.md`, `MAINTAINERS.md`, `.github/CODEOWNERS` | +| `documentation_roadmap` | `ROADMAP.md` | +| `documentation_architecture` | `docs/architecture.md` | +| `documentation_security` | `docs/security-requirements.md` | +| `documentation_quick_start` | README Quick Start | +| `documentation_current` | Corrected rule counts and support documentation in issue #199 PR | +| `documentation_achievements` | README Security Assurance section | +| `maintenance_or_update` | `SUPPORT.md`, `CHANGELOG.md`, database migration documentation | +| `report_tracker` | Public GitHub Issues | +| `vulnerability_response_process` | `.github/SECURITY.md` | +| `vulnerability_report_credit` | `SECURITY_ACKNOWLEDGEMENTS.md`; currently N/A because no external disclosure is recorded | +| `coding_standards` | `CONTRIBUTING.md` | +| `coding_standards_enforced` | Ruff/ESLint jobs in `.github/workflows/ci.yml` | +| `installation_common` | README Docker and source installation instructions | +| `installation_development_quick` | `CONTRIBUTING.md` Local Dev Setup | +| `external_dependencies` | `requirements.txt` and npm lockfiles | +| `dependency_monitoring` | Dependabot, Dependency Review, pip-audit and Trivy workflows | +| `updateable_reused_components` | Pinned dependency manifests and standard package managers | +| `automated_integration_testing` | GitHub Actions CI on every pull request | +| `test_statement_coverage80` | CI `--cov-fail-under=80` plus targeted Azure client tests | +| `test_policy_mandated` | Governance change process and contribution requirements | +| `tests_documented_added` | `CONTRIBUTING.md` pull-request requirements | +| `warnings_strict` | Ruff/ESLint zero-error CI gates | +| `implement_secure_design` | Security requirements and assurance case | +| `crypto_weaknesses` | Production JWT secret validation and standard maintained TLS clients | +| `crypto_credential_agility` | Runtime environment configuration without recompilation | +| `crypto_used_network` | HTTPS deployments and secure provider endpoints | +| `crypto_tls12` | Hosted HTTPS endpoints and maintained TLS libraries | +| `crypto_certificate_verification` | Standard verification defaults; no disabled verification in source | +| `crypto_verification_private` | Verification occurs in the TLS client before HTTP data is sent | +| `hardening` | Website/frontend CSP and security headers, production fail-closed configuration | +| `assurance_case` | `docs/security-assurance-case.md` | +| `static_analysis_common_vulnerabilities` | CodeQL, Bandit and Semgrep | +| `dynamic_analysis_unsafe` | N/A: project code is Python/JavaScript, not C/C++ | + +## Owner or operational confirmation required + +| Criterion | Required confirmation/action | +|---|---| +| `dco` | Project lead approves DCO policy and enables the merge check | +| `access_continuity` | Confirm two people hold real issue, merge, release and recovery capability | +| `bus_factor` | Confirm capability is distributed, not only documented in CODEOWNERS | +| `signed_releases` | Select and operate the signing/attestation process in `docs/release-security.md` | +| `version_tags_signed` | Create future important tags as signed annotated tags or approve equivalent attestation | +| `vulnerability_report_credit` | Confirm the acknowledgement record is complete before selecting N/A | + +## Evidence or implementation still incomplete + +| Criterion | Gap | +|---|---| +| `accessibility_best_practices` | Policy exists; complete keyboard/semantic/WCAG review and record results | +| `internationalization` | English-only UI; implement localization or mark Unmet with justification | +| `regression_tests_added50` | Preliminary audit shows 9 of 12 fixes with test changes; verify behavioral assertions before marking Met | +| `interfaces_current` | Review deprecated API warnings and document the periodic check | +| `input_validation` | Complete route-by-route allowlist audit and close discovered gaps | +| `crypto_algorithm_agility` | Review JWT and signing algorithm agility; document supported migration path | +| `build_repeatable` | Demonstrate repeatable frontend/release output or provide an accurate scripting-language N/A rationale | + +## Likely N/A after owner review + +- `sites_password_security`: OpenShield does not operate its own external-user + password database; authentication uses JWT/deployment identity mechanisms. +- `build_standard_variables`, `build_preserve_debug`, and + `build_non_recursive`: no native C/C++ build is produced. +- `installation_standard_variables`: source/container deployment does not use a + system-wide POSIX installer or `DESTDIR` installation step. + +N/A answers must explain the actual architecture. They must not be used to hide +missing work. diff --git a/docs/regression-test-audit.md b/docs/regression-test-audit.md new file mode 100644 index 0000000..4449e32 --- /dev/null +++ b/docs/regression-test-audit.md @@ -0,0 +1,33 @@ +# Regression Test Audit + +OpenShield requires reproducible bug fixes to include automated regression +tests. This audit covers merged bug-fix pull requests from the project's start +through 16 July 2026, within the six-month OpenSSF Silver review window. + +The audit searched merged pull requests whose title identified a fix and checked +whether the pull request changed a file under `tests/` or another automated test +file. Release-only aggregation PRs were not counted as individual bug fixes. + +| Pull request | Automated test changed? | +|---|---| +| #108 scanner abstraction fix | No | +| #130 compliance mapping fix | No | +| #143 Flask test-safety/CI fix | No separate regression file | +| #145 latest-scan endpoint fix | Yes | +| #162 reliability fixes | Yes | +| #166 compliance, TLS and pagination fixes | Yes | +| #169 async scan recovery fix | Yes | +| #175 container vulnerability fix | Yes | +| #183 CodeQL security fixes | Yes | +| #184 identity logging fix | Yes | +| #185 Semgrep workflow fixes | Yes | +| #188 XSS and AI-key fixes | Yes | + +Nine of twelve audited bug-fix pull requests changed automated tests: **75% by +file-change evidence**. This is above the Silver 50% threshold, subject to a +maintainer confirming that the changed tests reproduce the corrected behavior. + +This preliminary repository-history audit is not a claim that every test fully +exercises every fixed behavior. Maintainers must verify the behavioral +assertions before entering this criterion as Met, and refresh the audit before +each OpenSSF assessment. diff --git a/docs/release-security.md b/docs/release-security.md new file mode 100644 index 0000000..505674a --- /dev/null +++ b/docs/release-security.md @@ -0,0 +1,37 @@ +# Release Security and Verification + +## Current process + +Version tags trigger `.github/workflows/release.yml`, which creates GitHub +release notes. Published releases trigger `.github/workflows/sbom-release.yml`, +which generates and uploads a CycloneDX SBOM. Release actions are pinned to +specific commits. + +## Required signing process + +OpenShield must not mark the OpenSSF `signed_releases` criterion Met until the +following process is implemented and exercised for a real release: + +1. Build a deterministic source or distribution archive from the reviewed tag. +2. Generate SHA-256 checksums and a cryptographic signature or keyless Sigstore + attestation for each distributed artifact and its SBOM. +3. Keep signing authority separate from the public download location. A keyless + workflow must bind the identity, repository, workflow and commit in the + transparency record. +4. Upload artifacts, checksums, signatures/attestations and verification + instructions to the release. +5. Verify the published artifacts in a separate CI step before announcing the + release. +6. Create important release tags as signed annotated tags, or document why the + artifact-attestation mechanism provides the authoritative release identity. + +The project lead must approve the signing model and access policy before this is +automated. Existing `v0.1.0` and `v0.3.0` tags are lightweight tags and do not +by themselves satisfy signed-release requirements. + +## User verification target + +The final workflow must provide copyable verification commands that validate +the artifact digest, signer identity, repository and workflow identity. Those +commands will replace this section once the signing mechanism is selected and +tested. diff --git a/docs/rules-reference.md b/docs/rules-reference.md index e8cd964..a79511f 100644 --- a/docs/rules-reference.md +++ b/docs/rules-reference.md @@ -1,6 +1,6 @@ # Rules Reference -OpenShield currently ships 44 Azure scan rules. This table is generated from the module-level constants in `scanner/rules/`. +OpenShield currently ships 51 Azure scan rules. This table is generated from the module-level constants in `scanner/rules/`. | Rule ID | Name | Severity | Category | CIS | NIST | ISO 27001 | |---|---|---|---|---|---|---| @@ -40,6 +40,7 @@ OpenShield currently ships 44 Azure scan rules. This table is generated from the | AZ-NET-012 | NSG Flow Logs Not Enabled | MEDIUM | Network | 6.7 | DE.CM-1 | A.12.4.1 | | AZ-NET-013 | Azure Firewall Not Enabled on Virtual Network | HIGH | Network | 6.4 | PR.AC-5 | A.13.1.1 | | AZ-NET-014 | VNet Peering Configured Without Gateway Transit Restrictions | MEDIUM | Network | 6.6 | PR.AC-5 | A.13.1.1 | +| AZ-NET-015 | Public DNS Zone Exposes Internal Infrastructure Details | MEDIUM | Network | 9.8 | PR.AC-5 | A.13.1.1 | | AZ-PQC-001 | TLS Using Classical Key Exchange Algorithm | HIGH | PostQuantum | 9.9 | PR.DS-2 | A.10.1.1 | | AZ-PQC-002 | Key Vault Key Using Non-Quantum-Safe Algorithm | HIGH | PostQuantum | 8.1 | PR.DS-2 | A.10.1.1 | | AZ-PQC-003 | Key Vault Certificate Using Non-Quantum-Safe Signature Algorithm | MEDIUM | PostQuantum | 8.9 | PR.DS-2 | A.10.1.1 | diff --git a/docs/security-assurance-case.md b/docs/security-assurance-case.md new file mode 100644 index 0000000..895a328 --- /dev/null +++ b/docs/security-assurance-case.md @@ -0,0 +1,78 @@ +# OpenShield Security Assurance Case + +## Claim + +When deployed according to the documented production requirements, OpenShield +provides a defensible, least-privilege Azure configuration assessment without +intentionally collecting workload secrets or contents. This claim is bounded by +the limitations in `docs/security-requirements.md`. + +## Assets and threats + +Protected assets include Azure credentials, JWT signing material, scan results, +resource metadata, database records, remediation authority and release +integrity. Relevant threat actors include unauthenticated internet clients, +malicious or compromised contributors, compromised dependencies, overprivileged +cloud identities, malicious insiders and attackers controlling Azure API data. + +Primary threats are unauthorized scan execution or data modification, secret +disclosure, injection, path traversal, server-side request abuse, forged tokens, +unsafe remediation, dependency compromise, false compliance and tampered +releases. + +## Trust boundaries + +```text +Browser ──HTTPS──> Frontend ──HTTPS──> Flask API ──> PostgreSQL + │ + ├──> Azure management APIs / Graph + ├──> NVD and configured AI providers + └──> optional Sentinel ingestion + +Contributor ──pull request──> GitHub review and CI ──> protected project code +Operator ──explicit confirmation──> remediation playbook ──> Azure resource +``` + +Data crossing each boundary is untrusted until authenticated and validated. +Azure and third-party API responses are also treated as potentially malformed +or incomplete. + +## Secure design argument + +- **Least privilege:** CI permissions are scoped per workflow; Azure checks use + management metadata and document minimum read permissions. +- **Fail-safe defaults:** production rejects weak JWT configuration; API or + permission failures do not silently become compliant results. +- **Complete mediation:** protected write routes apply authentication centrally. +- **Separation of privilege:** changes require review and CI; destructive + remediation requires explicit operator confirmation; security disclosures use + a private process. +- **Economy of mechanism:** scanner rules use a shared Azure client and normalized + finding schema rather than embedding credentials or clients in every rule. +- **Open design:** policies, source, workflows, tests and security evidence are + public and reviewable. +- **Minimize sensitive data:** findings contain the configuration context needed + for remediation, not secrets or customer workload contents. + +## Implementation weakness argument + +- Injection and malformed input are countered with typed JSON handling, + allowlist validation, path normalization and tests for security boundaries. +- Authentication weaknesses are countered by production secret validation, + token verification and protected state-changing routes. +- Supply-chain risks are countered by pinned GitHub Actions, dependency review, + pip-audit, Dependabot, SBOM generation, Trivy and release review. +- Code weaknesses are checked using Ruff, CodeQL, Bandit, Semgrep, Gitleaks and + automated regression tests. +- Browser risks are reduced by CSP and other security headers on deployed web + surfaces. +- Availability risks from remediation are reduced through validation, warnings + and explicit confirmation rather than automatic changes. + +## Residual risk and evidence review + +Cloud APIs can change, checks can be incomplete, mappings can be contextual, and +maintainers or dependencies can be compromised. Findings therefore support—not +replace—professional risk assessment. Evidence is reviewed through CI, release +work, security advisories and the OpenSSF assessment. Gaps remain tracked in +issue #199 until the project can support each claim with current public proof. diff --git a/docs/security-requirements.md b/docs/security-requirements.md new file mode 100644 index 0000000..5254935 --- /dev/null +++ b/docs/security-requirements.md @@ -0,0 +1,49 @@ +# OpenShield Security Requirements + +## Security objectives + +OpenShield must: + +- authenticate state-changing API operations and fail closed in production when + the JWT secret is missing, weak or set to the development default; +- request only the Azure management-plane and Microsoft Graph permissions + documented for enabled checks; +- avoid collecting secrets, credentials, application settings, customer data or + workload contents when configuration metadata is sufficient; +- validate restricted untrusted inputs using allowlists, length limits and + typed parsing before they reach files, databases, subprocesses or external + services; +- preserve API and permission failures as unknown rather than reporting false + compliance; +- protect network communication using HTTPS/TLS with certificate verification; +- keep dependencies, workflows and release contents reviewable and + machine-inventoried; +- log security-relevant failures without disclosing credentials or sensitive + provider responses. + +## Expected deployment controls + +Operators are responsible for TLS termination, database access control, secret +storage, log retention, backup and recovery, Azure least-privilege role +assignment, and restricting administrative access to trusted users and +networks. Production deployments must use unique secrets and supported +dependencies. + +## Security limitations + +OpenShield is a posture assessment aid, not a penetration test, formal audit or +guarantee of security. A clean scan does not prove that an Azure environment is +secure. Rules cover documented configuration states and may be limited by API +permissions, service tiers, Azure API behavior and organizational context. +Remediation playbooks can affect availability and require operator review. + +OpenShield does not claim that its framework mappings constitute certification +or complete benchmark coverage. Unsupported versions receive no guaranteed +security fixes. + +## Verification evidence + +The security policy, architecture, assurance case, automated test suite, SAST, +secret scanning, dependency review, SBOM generation and container scanning form +the public evidence for these requirements. Known defects must be tracked and +resolved through GitHub issues or private advisories as appropriate. diff --git a/frontend/package.json b/frontend/package.json index 62091c6..631146a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", - "lint": "eslint .", + "lint": "eslint . --max-warnings=0", "preview": "vite preview" }, "dependencies": { diff --git a/frontend/src/components/ai/ChatPanel.jsx b/frontend/src/components/ai/ChatPanel.jsx index 9566694..5a302e4 100644 --- a/frontend/src/components/ai/ChatPanel.jsx +++ b/frontend/src/components/ai/ChatPanel.jsx @@ -37,7 +37,7 @@ const ChatPanel = forwardRef(function ChatPanel({ initialMessages = [], contextF timestamp: new Date().toISOString(), }; setMessages((prev) => [...prev, msg]); - }, [contextFinding?.ruleId]); + }, [contextFinding]); async function handleSend(text) { if (!text?.trim()) return; diff --git a/frontend/src/components/layout/Header.jsx b/frontend/src/components/layout/Header.jsx index 8d9770d..feb635f 100644 --- a/frontend/src/components/layout/Header.jsx +++ b/frontend/src/components/layout/Header.jsx @@ -63,7 +63,7 @@ function ScanToast({ result, error, onClose }) { useEffect(() => { const t = setTimeout(onClose, 6000); return () => clearTimeout(t); - }, []); + }, [onClose]); const isSuccess = !!result; diff --git a/frontend/src/pages/AILayer.jsx b/frontend/src/pages/AILayer.jsx index 63afa18..ed37840 100644 --- a/frontend/src/pages/AILayer.jsx +++ b/frontend/src/pages/AILayer.jsx @@ -98,13 +98,13 @@ export default function AILayer() { const [cveData, setCveData] = useState(null); const [cveLoading, setCveLoading] = useState(true); const [settingsKey, setSettingsKey] = useState(0); // force re-render on settings save + const initialFinding = location.state?.finding; useEffect(() => { api.getFindings() .then((scans) => { setFindings(scans); - const fromScan = location.state?.finding; - if (fromScan) setSelectedFinding(fromScan); + if (initialFinding) setSelectedFinding(initialFinding); aiApi.getSummary(scans).then(setSummary).finally(() => setSummaryLoading(false)); }) .catch(() => { @@ -112,7 +112,7 @@ export default function AILayer() { aiApi.getSummary([]).then(setSummary).finally(() => setSummaryLoading(false)); }); aiApi.getCVEAnalysis().then(setCveData).finally(() => setCveLoading(false)); - }, []); + }, [initialFinding]); const handleFindingSelect = (f) => setSelectedFinding((prev) => (prev?.id === f.id ? null : f)); diff --git a/frontend/src/pages/DetailedScan.jsx b/frontend/src/pages/DetailedScan.jsx index a2a7e66..41c8fee 100644 --- a/frontend/src/pages/DetailedScan.jsx +++ b/frontend/src/pages/DetailedScan.jsx @@ -81,18 +81,18 @@ export default function DetailedScan() { const [selected, setSelected] = useState(null); const [playbook, setPlaybook] = useState(null); const [showBanner, setShowBanner] = useState(!!location.state?.fromPrioritization); + const preSelectRuleId = location.state?.ruleId; // Load findings on mount useEffect(() => { api.getFindings().then((data) => { setFindings(data); - const preSelectRuleId = location.state?.ruleId; const initial = preSelectRuleId ? (data.find((f) => f.ruleId === preSelectRuleId) ?? data[0]) : data[0]; selectFinding(initial); }); - }, []); + }, [preSelectRuleId]); // Fetch playbook whenever selected finding changes async function selectFinding(f) { diff --git a/tests/test_azure_client_management.py b/tests/test_azure_client_management.py new file mode 100644 index 0000000..5d8556e --- /dev/null +++ b/tests/test_azure_client_management.py @@ -0,0 +1,96 @@ +"""Direct tests for AzureClient management-plane wrappers and failure states.""" + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from scanner.azure_client import AzureClient + + +@pytest.fixture +def client(): + return AzureClient("sub-1", credential=MagicMock()) + + +def test_parse_resource_id_handles_full_and_short_ids(): + full = "/subscriptions/s/resourceGroups/RG/providers/Microsoft.Web/sites/app" + assert AzureClient.parse_resource_id(full) == {"resource_group": "RG", "name": "app"} + assert AzureClient.parse_resource_id("app") == {"name": "app"} + + +@pytest.mark.parametrize( + ("method", "patch_target", "operation"), + [ + ("get_storage_accounts", "scanner.azure_client.StorageManagementClient", "storage_accounts.list"), + ( + "get_network_security_groups", + "scanner.azure_client.NetworkManagementClient", + "network_security_groups.list_all", + ), + ("get_virtual_networks", "scanner.azure_client.NetworkManagementClient", "virtual_networks.list_all"), + ("get_public_ip_addresses", "scanner.azure_client.NetworkManagementClient", "public_ip_addresses.list_all"), + ("get_load_balancers", "scanner.azure_client.NetworkManagementClient", "load_balancers.list_all"), + ("get_virtual_machines", "scanner.azure_client.ComputeManagementClient", "virtual_machines.list_all"), + ("get_postgresql_servers", "scanner.azure_client.PostgreSQLManagementClient", "servers.list"), + ("get_sql_servers", "scanner.azure_client.SqlManagementClient", "servers.list"), + ("get_key_vaults", "scanner.azure_client.KeyVaultManagementClient", "vaults.list_by_subscription"), + ], +) +def test_list_wrappers_return_results_and_fail_closed(client, method, patch_target, operation): + with patch(patch_target) as constructor: + sdk = constructor.return_value + target = sdk + parts = operation.split(".") + for part in parts[:-1]: + target = getattr(target, part) + call = getattr(target, parts[-1]) + call.return_value = [SimpleNamespace(name="one")] + assert [item.name for item in getattr(client, method)()] == ["one"] + + call.side_effect = RuntimeError("Azure unavailable") + assert getattr(client, method)() == [] + + +def test_single_resource_and_policy_wrappers(client): + with patch("scanner.azure_client.NetworkManagementClient") as constructor: + constructor.return_value.network_interfaces.get.return_value = SimpleNamespace(name="nic") + assert client.get_network_interface("rg", "nic").name == "nic" + constructor.return_value.network_interfaces.get.side_effect = RuntimeError("denied") + assert client.get_network_interface("rg", "nic") is None + + with patch("scanner.azure_client.SqlManagementClient") as constructor: + policy = SimpleNamespace(state="Enabled") + constructor.return_value.server_blob_auditing_policies.get.return_value = policy + assert client.get_sql_server_auditing_policy("rg", "sql") is policy + constructor.return_value.server_blob_auditing_policies.get.side_effect = RuntimeError("denied") + assert client.get_sql_server_auditing_policy("rg", "sql") is None + + +def test_firewall_and_peering_wrappers(client): + with patch("scanner.azure_client.NetworkManagementClient") as constructor: + sdk = constructor.return_value + sdk.azure_firewalls.list.return_value = [SimpleNamespace(name="fw")] + sdk.azure_firewalls.list_all.return_value = [SimpleNamespace(name="fw-all")] + sdk.virtual_network_peerings.list.return_value = [SimpleNamespace(name="peer")] + + assert client.get_azure_firewalls("rg")[0].name == "fw" + assert client.get_all_azure_firewalls()[0].name == "fw-all" + assert client.get_vnet_peerings("rg", "vnet")[0].name == "peer" + + sdk.azure_firewalls.list.side_effect = RuntimeError("denied") + sdk.azure_firewalls.list_all.side_effect = RuntimeError("denied") + sdk.virtual_network_peerings.list.side_effect = RuntimeError("denied") + assert client.get_azure_firewalls("rg") == [] + assert client.get_all_azure_firewalls() is None + assert client.get_vnet_peerings("rg", "vnet") == [] + + +def test_vm_extensions_normalize_sdk_page_and_failure(client): + with patch("scanner.azure_client.ComputeManagementClient") as constructor: + constructor.return_value.virtual_machine_extensions.list.return_value = SimpleNamespace( + value=[SimpleNamespace(name="agent")] + ) + assert client.get_vm_extensions("rg", "vm")[0].name == "agent" + constructor.return_value.virtual_machine_extensions.list.side_effect = RuntimeError("denied") + assert client.get_vm_extensions("rg", "vm") is None diff --git a/website/content.js b/website/content.js index e0d989b..72af029 100644 --- a/website/content.js +++ b/website/content.js @@ -82,6 +82,11 @@ const siteContent = { {"id": "AZ-IDN-002", "name": "No MFA Enforced on Admin Accounts via Conditional Access", "severity": "HIGH", "category": "Identity", "description": "No Conditional Access policy is enabled that requires multi-factor authentication for administrator accounts. Without MFA enforcement, a single compromised password is sufficient for an attacker to gain privileged access.", "frameworks": {"CIS": "1.2.4", "NIST": "PR.AC-1", "ISO27001": "A.9.4.2"}}, {"id": "AZ-IDN-003", "name": "Guest User Invitations Not Restricted to Admins in Entra ID", "severity": "MEDIUM", "category": "Identity", "description": "Guest user invitations in Entra ID are not restricted to administrators. Any organisation member can invite external users without centralised review, bypassing formal external identity provisioning controls.", "frameworks": {"CIS": "1.15", "NIST": "PR.AC-1", "ISO27001": "A.9.2.1"}}, {"id": "AZ-IDN-004", "name": "No Privileged Identity Management for Admin Roles", "severity": "HIGH", "category": "Identity", "description": "Privileged Identity Management (PIM) is not configured for one or more admin roles in Entra ID. Without PIM, admin roles are permanently assigned with no just-in-time access controls or time-bound activation.", "frameworks": {"CIS": "1.16", "NIST": "PR.AC-4", "ISO27001": "A.9.2.3", "SOC2": "CC6.3"}}, + {"id": "AZ-IDN-005", "name": "Guest User with High Privilege Role in Entra ID", "severity": "HIGH", "category": "Identity", "description": "A guest identity holds a privileged directory role, increasing the impact of external account compromise.", "frameworks": {"CIS": "1.3", "NIST": "PR.AC-4", "ISO27001": "A.9.2.3"}}, + {"id": "AZ-IDN-006", "name": "Service Principal Client Secret Older Than 90 Days", "severity": "HIGH", "category": "Identity", "description": "A service principal credential is older than the approved rotation period.", "frameworks": {"CIS": "1.14", "NIST": "PR.AC-1", "ISO27001": "A.9.4.3"}}, + {"id": "AZ-IDN-007", "name": "Active User with No MFA Registered in Entra ID", "severity": "HIGH", "category": "Identity", "description": "An active member account has no registered multi-factor authentication method.", "frameworks": {"CIS": "1.1", "NIST": "PR.AC-7", "ISO27001": "A.9.4.2"}}, + {"id": "AZ-IDN-008", "name": "Custom RBAC Role with Wildcard Permissions at Subscription Scope", "severity": "HIGH", "category": "Identity", "description": "A custom role grants wildcard permissions at subscription scope and can exceed intended least privilege.", "frameworks": {"CIS": "1.23", "NIST": "PR.AC-4", "ISO27001": "A.9.2.3"}}, + {"id": "AZ-IDN-009", "name": "No Activity Log Alert for Role Assignment Changes", "severity": "MEDIUM", "category": "Identity", "description": "No enabled activity-log alert detects changes to Azure role assignments.", "frameworks": {"CIS": "5.2.1", "NIST": "DE.CM-3", "ISO27001": "A.12.4.1"}}, // KeyVault (5) {"id": "AZ-KV-001", "name": "Key Vault with Soft Delete Disabled", "severity": "MEDIUM", "category": "KeyVault", "description": "Azure Key Vault soft delete is disabled. Without soft delete, secrets, keys, and certificates can be permanently destroyed immediately upon deletion by accident, a disgruntled insider, or an attacker.", "frameworks": {"CIS": "8.8", "NIST": "PR.IP-4", "ISO27001": "A.17.2.1"}}, {"id": "AZ-KV-002", "name": "Key Vault Allows Public Network Access Without Private Endpoint", "severity": "HIGH", "category": "KeyVault", "description": "The Azure Key Vault is accessible over the public internet without a private endpoint configured. This increases the risk of unauthorized access to sensitive secrets, keys, and certificates.", "frameworks": {"CIS": "8.7", "NIST": "AC-17", "ISO27001": "A.13.1.1"}}, @@ -103,6 +108,7 @@ const siteContent = { {"id": "AZ-NET-012", "name": "NSG Flow Logs Not Enabled", "severity": "MEDIUM", "category": "Network", "description": "Network Security Group flow logs are not enabled. Without flow logs, network traffic is not auditable and attacker movement through the network cannot be reconstructed.", "frameworks": {"CIS": "6.7", "NIST": "DE.CM-1", "ISO27001": "A.12.4.1", "SOC2": "CC7.2"}}, {"id": "AZ-NET-013", "name": "Azure Firewall Not Enabled on Virtual Network", "severity": "HIGH", "category": "Network", "description": "The virtual network has no Azure Firewall deployed or associated. Relying only on NSGs leaves the network without a centralized perimeter inspection, logging, and threat-filtering layer.", "frameworks": {"CIS": "6.4", "NIST": "PR.AC-5", "ISO27001": "A.13.1.1", "SOC2": "CC6.6"}}, {"id": "AZ-NET-014", "name": "VNet Peering Configured Without Gateway Transit Restrictions", "severity": "MEDIUM", "category": "Network", "description": "A Virtual Network peering connection has gateway transit enabled, potentially enabling lateral movement between network zones that should be isolated from each other.", "frameworks": {"CIS": "6.6", "NIST": "PR.AC-5", "ISO27001": "A.13.1.1", "SOC2": "CC6.6"}}, + {"id": "AZ-NET-015", "name": "Public DNS Zone Exposes Internal Infrastructure Details", "severity": "MEDIUM", "category": "Network", "description": "A public DNS zone exposes private RFC1918 addresses or internal-service names that assist infrastructure reconnaissance.", "frameworks": {"CIS": "9.8", "NIST": "PR.AC-5", "ISO27001": "A.13.1.1", "SOC2": "CC6.6"}}, // Storage (5) {"id": "AZ-STOR-001", "name": "Public Blob Access Enabled on Storage Account", "severity": "HIGH", "category": "Storage", "description": "Storage accounts with public blob access enabled allow unauthenticated read access to blob data over the internet. This setting can expose sensitive files, backups, or configuration data to any external actor.", "frameworks": {"CIS": "3.5", "NIST": "PR.AC-3", "ISO27001": "A.9.4.1"}}, {"id": "AZ-STOR-002", "name": "Storage Account Allows HTTP Traffic (Not HTTPS-Only)", "severity": "HIGH", "category": "Storage", "description": "Storage accounts that do not enforce HTTPS-only traffic allow data to be transmitted in plaintext over HTTP. This exposes credentials and data to man-in-the-middle attacks and interception.", "frameworks": {"CIS": "3.1", "NIST": "PR.DS-2", "ISO27001": "A.10.1.1"}}, @@ -112,7 +118,13 @@ const siteContent = { // Post-Quantum Cryptography (3) — unique to OpenShield; no competitor OSS CSPM tool scans for quantum-vulnerable assets {"id": "AZ-PQC-001", "name": "TLS Using Classical Key Exchange Algorithm", "severity": "HIGH", "category": "PostQuantum", "description": "The resource is configured with TLS using classical key exchange algorithms such as RSA or ECDH. Adversaries are executing Harvest Now Decrypt Later attacks — collecting encrypted traffic today to decrypt once quantum computers are available. Maps to NIST FIPS 203 (ML-KEM) migration requirements.", "frameworks": {"CIS": "9.9", "NIST": "PR.DS-2", "ISO27001": "A.10.1.1", "SOC2": "CC6.7"}}, {"id": "AZ-PQC-002", "name": "Key Vault Key Using Non-Quantum-Safe Algorithm", "severity": "HIGH", "category": "PostQuantum", "description": "The Key Vault contains RSA or ECC keys vulnerable to Shor's algorithm on quantum computers. A sufficiently powerful quantum computer can break these keys, compromising all data encrypted or signed with them. Maps to NIST FIPS 204 (ML-DSA) migration requirements.", "frameworks": {"CIS": "8.1", "NIST": "PR.DS-2", "ISO27001": "A.10.1.2", "SOC2": "CC6.7"}}, - {"id": "AZ-PQC-003", "name": "Key Vault Certificate Using Non-Quantum-Safe Signature Algorithm", "severity": "MEDIUM", "category": "PostQuantum", "description": "The Key Vault contains certificates signed using RSA or ECDSA algorithms, vulnerable to Shor's algorithm. Certificates used for TLS, authentication, and code signing will need migration to ML-DSA (FIPS 204) or SLH-DSA (FIPS 205) as certificate authorities add post-quantum support.", "frameworks": {"CIS": "8.9", "NIST": "PR.DS-2", "ISO27001": "A.10.1.2", "SOC2": "CC6.7"}} + {"id": "AZ-PQC-003", "name": "Key Vault Certificate Using Non-Quantum-Safe Signature Algorithm", "severity": "MEDIUM", "category": "PostQuantum", "description": "The Key Vault contains certificates signed using RSA or ECDSA algorithms, vulnerable to Shor's algorithm. Certificates used for TLS, authentication, and code signing will need migration to ML-DSA (FIPS 204) or SLH-DSA (FIPS 205) as certificate authorities add post-quantum support.", "frameworks": {"CIS": "8.9", "NIST": "PR.DS-2", "ISO27001": "A.10.1.2", "SOC2": "CC6.7"}}, + {"id": "AZ-AKS-001", "name": "AKS Private Cluster Not Enabled", "severity": "HIGH", "category": "Kubernetes", "description": "The AKS control-plane API is reachable through a public endpoint.", "frameworks": {"CIS": "N/A-AKS-001", "NIST": "PR.AC-3", "ISO27001": "A.13.1.1"}}, + {"id": "AZ-AKS-002", "name": "AKS Local Accounts Enabled", "severity": "HIGH", "category": "Kubernetes", "description": "Local AKS accounts permit authentication outside centrally governed Entra identities.", "frameworks": {"CIS": "N/A-AKS-002", "NIST": "PR.AC-1", "ISO27001": "A.9.2.1"}}, + {"id": "AZ-AKS-003", "name": "AKS Cluster Not Using Managed Identity", "severity": "HIGH", "category": "Kubernetes", "description": "The AKS control plane does not use an Azure managed identity.", "frameworks": {"CIS": "N/A-AKS-003", "NIST": "PR.AC-1", "ISO27001": "A.9.2.1"}}, + {"id": "AZ-AKS-004", "name": "AKS Workload Identity Not Fully Enabled", "severity": "MEDIUM", "category": "Kubernetes", "description": "OIDC issuer or Workload Identity support is not fully enabled for the cluster.", "frameworks": {"CIS": "N/A-AKS-004", "NIST": "PR.AC-4", "ISO27001": "A.9.2.3"}}, + {"id": "AZ-AKS-005", "name": "AKS Azure Policy Add-on Not Enabled", "severity": "MEDIUM", "category": "Kubernetes", "description": "The cluster does not use the Azure Policy add-on for centralized governance.", "frameworks": {"CIS": "N/A-AKS-005", "NIST": "PR.IP-1", "ISO27001": "A.12.1.2"}}, + {"id": "AZ-AKS-006", "name": "AKS Node OS Automatic Upgrades Disabled", "severity": "HIGH", "category": "Kubernetes", "description": "The cluster lacks a managed node operating-system security upgrade channel.", "frameworks": {"CIS": "N/A-AKS-006", "NIST": "PR.IP-12", "ISO27001": "A.12.6.1"}} ], ecosystem: [ { @@ -464,7 +476,7 @@ const siteContent = { "Flask REST API with JWT authentication and CORS", "Scanner engine with 20 Azure misconfiguration rules across Storage, Network, Identity, Database, Compute, and Key Vault", "Compliance framework mappings for CIS Azure Benchmark, NIST CSF, ISO 27001, and SOC 2", - "36 Azure CLI remediation playbooks — one per scanner rule", + "51 Azure CLI remediation playbooks — one per scanner rule", "PostgreSQL persistence for scan history and findings", "Microsoft Sentinel integration via Log Analytics custom table and KQL analytics rules", "GitHub Actions CI pipeline with 7 automated checks" From 94320d2958bdc515c99602e6ea4b49fdf753799c Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Thu, 16 Jul 2026 16:38:02 +0100 Subject: [PATCH 2/2] feat(frontend): add accessibility and i18n foundations --- .github/workflows/ci.yml | 3 ++ docs/accessibility-audit.md | 38 ++++++++++++++ docs/internationalization.md | 22 ++++++++ frontend/package.json | 4 +- frontend/scripts/accessibility-check.mjs | 27 ++++++++++ frontend/src/App.jsx | 11 ++-- frontend/src/components/ai/ChatPanel.jsx | 2 +- frontend/src/components/layout/Header.jsx | 61 +++++++++++++--------- frontend/src/components/layout/Layout.jsx | 11 +++- frontend/src/components/layout/Sidebar.jsx | 46 +++++++++------- frontend/src/components/shared/Card.jsx | 13 +++-- frontend/src/contexts/I18nContext.jsx | 36 +++++++++++++ frontend/src/i18n/I18nState.js | 9 ++++ frontend/src/i18n/messages.js | 41 +++++++++++++++ frontend/src/i18n/messages.test.mjs | 11 ++++ frontend/src/pages/AILayer.jsx | 6 +-- frontend/src/pages/DetailedScan.jsx | 4 +- 17 files changed, 283 insertions(+), 62 deletions(-) create mode 100644 docs/accessibility-audit.md create mode 100644 docs/internationalization.md create mode 100644 frontend/scripts/accessibility-check.mjs create mode 100644 frontend/src/contexts/I18nContext.jsx create mode 100644 frontend/src/i18n/I18nState.js create mode 100644 frontend/src/i18n/messages.js create mode 100644 frontend/src/i18n/messages.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ddbb30..ede2d68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -560,6 +560,9 @@ jobs: - name: Run aiSettings tests run: node src/utils/aiApi.test.mjs + - name: Run accessibility and internationalization checks + run: npm run test:a11y && npm run test:i18n + - name: Build run: npm run build diff --git a/docs/accessibility-audit.md b/docs/accessibility-audit.md new file mode 100644 index 0000000..026cfaa --- /dev/null +++ b/docs/accessibility-audit.md @@ -0,0 +1,38 @@ +# Accessibility Audit + +Assessment date: 16 July 2026. Scope: React dashboard and static project +website. Target: practical alignment with WCAG 2.2 AA; this is not a formal +conformance certification. + +## Controls added + +- A keyboard-visible skip link targets the dashboard's main content. +- Primary navigation and mobile navigation have accessible names. +- Decorative navigation icons are hidden from assistive technology. +- Icon-only close and status actions have accessible labels. +- Popovers and connection errors expose dialog semantics and names. +- Scan results and backend connectivity expose polite live status updates. +- The off-screen mobile navigation is inert while closed. +- A source-level CI check rejects positive tab order, non-semantic clickable + `div`/`span` elements, missing image alternative text, and missing document + language. + +## Keyboard review + +The expected keyboard path is: skip link, mobile menu when present, language +selector, scan control, primary navigation, then page content. Native buttons, +links, inputs and selects retain browser focus behavior. Escape handling remains +available in the scan input. A full screen-reader/browser matrix remains a +release-quality follow-up rather than a claim made by this audit. + +## Known limitations + +- Some data visualizations need separate screen-reader summaries as their + components evolve. +- Focus trapping and restoration for every future modal must be checked during + component review. +- Colour contrast should be rechecked whenever theme tokens change. +- The static website has its own editing surface and needs repeat manual review + when that interface changes. + +Run `npm run test:a11y` from `frontend/` for the automated source checks. diff --git a/docs/internationalization.md b/docs/internationalization.md new file mode 100644 index 0000000..f848083 --- /dev/null +++ b/docs/internationalization.md @@ -0,0 +1,22 @@ +# Internationalization + +The dashboard uses message catalogs through `I18nContext`. English is the +fallback language and Spanish demonstrates a second complete catalog for core +navigation, page titles, scan controls, status messages and theme controls. + +The selected locale is stored locally, applied to the document `lang` +attribute, and used with `Intl.DateTimeFormat` and `Intl.NumberFormat`. An +unsupported locale falls back to English. Security identifiers, Azure resource +names, findings and compliance control IDs are data and are never translated. + +## Adding a locale + +1. Add a catalog to `frontend/src/i18n/messages.js` using exactly the English + keys. +2. Translate meaning rather than word order; retain `{name}` placeholders. +3. Add the language's self-name to each catalog. +4. Run `npm run test:i18n`; missing keys fail the catalog test. +5. Review navigation at narrow and wide widths and verify date/number output. + +The website remains English-first. Additional website locales should reuse the +same terminology but must not duplicate security data or rule definitions. diff --git a/frontend/package.json b/frontend/package.json index 62091c6..f54699c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "vite", "build": "vite build", - "lint": "eslint .", + "lint": "eslint . --max-warnings=0", + "test:i18n": "node src/i18n/messages.test.mjs", + "test:a11y": "node scripts/accessibility-check.mjs", "preview": "vite preview" }, "dependencies": { diff --git a/frontend/scripts/accessibility-check.mjs b/frontend/scripts/accessibility-check.mjs new file mode 100644 index 0000000..35f8150 --- /dev/null +++ b/frontend/scripts/accessibility-check.mjs @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +const root = path.resolve(import.meta.dirname, '..'); +const sourceRoot = path.join(root, 'src'); + +function filesUnder(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(directory, entry.name); + return entry.isDirectory() ? filesUnder(full) : [full]; + }); +} + +const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8'); +assert.match(html, /]+lang="[a-z]{2}"/i, 'frontend HTML must declare a language'); + +for (const file of filesUnder(sourceRoot).filter((item) => /\.(jsx?|html)$/.test(item))) { + const source = fs.readFileSync(file, 'utf8'); + assert.doesNotMatch(source, /tabIndex=["'{]?[1-9]/, `${file} uses a positive tab order`); + assert.doesNotMatch(source, /<(div|span)\b[^>]*\bonClick=/, `${file} uses a non-semantic clickable element`); + for (const image of source.matchAll(/]*>/g)) { + assert.match(image[0], /\balt=/, `${file} contains an image without alt text`); + } +} + +console.log('accessibility static checks passed'); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 206e84f..6f9e3f0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { DarkModeProvider } from './contexts/DarkModeContext'; +import { I18nProvider } from './contexts/I18nContext'; import { api } from './utils/api'; import Layout from './components/layout/Layout'; import Discovery from './pages/Discovery'; @@ -25,8 +26,9 @@ export default function App() { return ( - - + + + }> } /> } /> @@ -37,8 +39,9 @@ export default function App() { } /> } /> - - + + + ); } diff --git a/frontend/src/components/ai/ChatPanel.jsx b/frontend/src/components/ai/ChatPanel.jsx index 9566694..5a302e4 100644 --- a/frontend/src/components/ai/ChatPanel.jsx +++ b/frontend/src/components/ai/ChatPanel.jsx @@ -37,7 +37,7 @@ const ChatPanel = forwardRef(function ChatPanel({ initialMessages = [], contextF timestamp: new Date().toISOString(), }; setMessages((prev) => [...prev, msg]); - }, [contextFinding?.ruleId]); + }, [contextFinding]); async function handleSend(text) { if (!text?.trim()) return; diff --git a/frontend/src/components/layout/Header.jsx b/frontend/src/components/layout/Header.jsx index 8d9770d..e6c52cc 100644 --- a/frontend/src/components/layout/Header.jsx +++ b/frontend/src/components/layout/Header.jsx @@ -5,23 +5,19 @@ import { FiLoader, FiZap, FiCheckCircle, FiAlertCircle, FiClock, } from 'react-icons/fi'; import { api } from '../../utils/api'; +import { useI18n } from '../../i18n/I18nState'; -const PAGE_TITLES = { - '/monitoring': { title: 'Security Monitoring', subtitle: 'Overall health score and trends' }, - '/discovery': { title: 'Resource Discovery', subtitle: 'All resources across your Azure environment' }, - '/prioritization': { title: 'Risk Prioritization', subtitle: 'What to fix first based on risk and effort' }, - '/scan': { title: 'Detailed Scan', subtitle: 'Findings with step-by-step remediation playbooks' }, - '/compliance': { title: 'Compliance', subtitle: 'Framework tracking and control status' }, - '/drift': { title: 'Configuration Drift', subtitle: 'Detect unexpected changes to your environment' }, - '/ai': { title: 'AI Assistant', subtitle: 'Ask questions about your security posture' }, +const PAGE_KEYS = { + '/monitoring': 'monitoring', '/discovery': 'discovery', '/prioritization': 'prioritization', + '/scan': 'scan', '/compliance': 'compliance', '/drift': 'drift', '/ai': 'ai', }; // ── Connection-error popup ───────────────────────────────────────────────── function ConnectionErrorPopup({ apiBase, onClose }) { return ( <> -
-
+
@@ -63,12 +59,12 @@ function ScanToast({ result, error, onClose }) { useEffect(() => { const t = setTimeout(onClose, 6000); return () => clearTimeout(t); - }, []); + }, [onClose]); const isSuccess = !!result; return ( -
+
-
@@ -126,10 +122,10 @@ function ScanInputPopover({ onConfirm, onCancel }) { return ( <> -
-
+ @@ -268,6 +268,15 @@ export default function Header({ onMenuToggle }) { {/* Right: controls */}
+ + {/* Run Scan button + popover wrapper */}
@@ -282,7 +291,7 @@ export default function Header({ onMenuToggle }) { : } - {scanning ? `Scanning… ${elapsed}s` : 'Run Scan'} + {scanning ? t('scan.scanning', { seconds: elapsed }) : t('scan.run')} @@ -298,12 +307,14 @@ export default function Header({ onMenuToggle }) { {lastScanAt && isLive && (
- Last scanned: {lastScanAt} + {t('scan.last', { date: lastScanAt })}
)} {/* Live / Reconnecting status dot */}
@@ -316,7 +327,7 @@ export default function Header({ onMenuToggle }) { )} - {isLive ? 'Live' : 'Reconnecting'} + {isLive ? t('status.live') : t('status.reconnecting')}
diff --git a/frontend/src/components/layout/Layout.jsx b/frontend/src/components/layout/Layout.jsx index 01a23c4..c2c4681 100644 --- a/frontend/src/components/layout/Layout.jsx +++ b/frontend/src/components/layout/Layout.jsx @@ -2,15 +2,22 @@ import { useState } from 'react'; import { Outlet } from 'react-router-dom'; import Sidebar from './Sidebar'; import Header from './Header'; +import { useI18n } from '../../i18n/I18nState'; export default function Layout() { const [sidebarOpen, setSidebarOpen] = useState(false); + const { t } = useI18n(); return (
+ + {t('skip.content')} + {/* Mobile overlay */} {sidebarOpen && ( -
setSidebarOpen(false)} /> @@ -20,7 +27,7 @@ export default function Layout() {
setSidebarOpen((v) => !v)} /> -
+
diff --git a/frontend/src/components/layout/Sidebar.jsx b/frontend/src/components/layout/Sidebar.jsx index e79074a..39461ed 100644 --- a/frontend/src/components/layout/Sidebar.jsx +++ b/frontend/src/components/layout/Sidebar.jsx @@ -4,31 +4,33 @@ import { FiShield, FiGitBranch, FiCpu, FiSun, FiMoon, FiX, } from 'react-icons/fi'; import { useDarkMode } from '../../contexts/DarkModeContext'; +import { useI18n } from '../../i18n/I18nState'; import Logo from '../shared/Logo'; const navItems = [ - { path: '/monitoring', label: 'Monitor', Icon: FiActivity }, - { path: '/discovery', label: 'Discover', Icon: FiSearch }, - { path: '/prioritization', label: 'Prioritize', Icon: FiTarget }, - { path: '/scan', label: 'Scan', Icon: FiZap }, - { path: '/compliance', label: 'Comply', Icon: FiShield }, - { path: '/drift', label: 'Drift', Icon: FiGitBranch }, - { path: '/ai', label: 'AI', Icon: FiCpu }, + { path: '/monitoring', key: 'monitoring', Icon: FiActivity }, + { path: '/discovery', key: 'discovery', Icon: FiSearch }, + { path: '/prioritization', key: 'prioritization', Icon: FiTarget }, + { path: '/scan', key: 'scan', Icon: FiZap }, + { path: '/compliance', key: 'compliance', Icon: FiShield }, + { path: '/drift', key: 'drift', Icon: FiGitBranch }, + { path: '/ai', key: 'ai', Icon: FiCpu }, ]; export default function Sidebar({ isOpen, onClose }) { const { isDark, toggle } = useDarkMode(); + const { t } = useI18n(); return ( <> {/* ── Desktop sidebar (always visible on lg+) ── */} -
{/* Drawer nav */} -
diff --git a/frontend/src/components/shared/Card.jsx b/frontend/src/components/shared/Card.jsx index bafe013..363fb70 100644 --- a/frontend/src/components/shared/Card.jsx +++ b/frontend/src/components/shared/Card.jsx @@ -1,10 +1,15 @@ export default function Card({ children, className = '', onClick }) { + const classes = `rounded-2xl border border-border-light dark:border-border-dark bg-bg-primary dark:bg-bg-dark-secondary p-6 shadow-soft hover:shadow-soft-lg transition-all duration-200 ${onClick ? 'cursor-pointer' : ''} ${className}`; + if (onClick) { + return ( + + ); + } return ( -
+
{children}
); diff --git a/frontend/src/contexts/I18nContext.jsx b/frontend/src/contexts/I18nContext.jsx new file mode 100644 index 0000000..f56e34f --- /dev/null +++ b/frontend/src/contexts/I18nContext.jsx @@ -0,0 +1,36 @@ +import { useEffect, useMemo, useState } from 'react'; +import { DEFAULT_LOCALE, messages, translate } from '../i18n/messages'; +import { I18nState } from '../i18n/I18nState'; +const STORAGE_KEY = 'openshield.locale'; + +function initialLocale() { + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored && messages[stored]) return stored; + const browserLocale = window.navigator.language?.split('-')[0]; + return messages[browserLocale] ? browserLocale : DEFAULT_LOCALE; +} + +export function I18nProvider({ children }) { + const [locale, setLocaleState] = useState(initialLocale); + + const setLocale = (nextLocale) => { + const supported = messages[nextLocale] ? nextLocale : DEFAULT_LOCALE; + window.localStorage.setItem(STORAGE_KEY, supported); + setLocaleState(supported); + }; + + useEffect(() => { + document.documentElement.lang = locale; + }, [locale]); + + const value = useMemo(() => ({ + locale, + locales: Object.keys(messages), + setLocale, + t: (key, values) => translate(locale, key, values), + formatDate: (value, options) => new Intl.DateTimeFormat(locale, options).format(new Date(value)), + formatNumber: (value, options) => new Intl.NumberFormat(locale, options).format(value), + }), [locale]); + + return {children}; +} diff --git a/frontend/src/i18n/I18nState.js b/frontend/src/i18n/I18nState.js new file mode 100644 index 0000000..5a4283f --- /dev/null +++ b/frontend/src/i18n/I18nState.js @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; + +export const I18nState = createContext(null); + +export function useI18n() { + const context = useContext(I18nState); + if (!context) throw new Error('useI18n must be used inside I18nProvider'); + return context; +} diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js new file mode 100644 index 0000000..dfe1ebe --- /dev/null +++ b/frontend/src/i18n/messages.js @@ -0,0 +1,41 @@ +export const DEFAULT_LOCALE = 'en'; + +export const messages = { + en: { + 'nav.monitoring': 'Monitor', 'nav.discovery': 'Discover', 'nav.prioritization': 'Prioritize', + 'nav.scan': 'Scan', 'nav.compliance': 'Comply', 'nav.drift': 'Drift', 'nav.ai': 'AI', + 'theme.dark': 'Dark mode', 'theme.light': 'Light mode', 'theme.toggle': 'Toggle colour theme', + 'menu.open': 'Open menu', 'menu.close': 'Close menu', 'nav.primary': 'Primary navigation', + 'language.label': 'Language', 'language.en': 'English', 'language.es': 'Español', + 'page.monitoring.title': 'Security Monitoring', 'page.monitoring.subtitle': 'Overall health score and trends', + 'page.discovery.title': 'Resource Discovery', 'page.discovery.subtitle': 'All resources across your Azure environment', + 'page.prioritization.title': 'Risk Prioritization', 'page.prioritization.subtitle': 'What to fix first based on risk and effort', + 'page.scan.title': 'Detailed Scan', 'page.scan.subtitle': 'Findings with step-by-step remediation playbooks', + 'page.compliance.title': 'Compliance', 'page.compliance.subtitle': 'Framework tracking and control status', + 'page.drift.title': 'Configuration Drift', 'page.drift.subtitle': 'Detect unexpected changes to your environment', + 'page.ai.title': 'AI Assistant', 'page.ai.subtitle': 'Ask questions about your security posture', + 'scan.run': 'Run Scan', 'scan.scanning': 'Scanning… {seconds}s', 'scan.last': 'Last scanned: {date}', + 'status.live': 'Live', 'status.reconnecting': 'Reconnecting', 'skip.content': 'Skip to main content', + }, + es: { + 'nav.monitoring': 'Monitorear', 'nav.discovery': 'Descubrir', 'nav.prioritization': 'Priorizar', + 'nav.scan': 'Escanear', 'nav.compliance': 'Cumplimiento', 'nav.drift': 'Cambios', 'nav.ai': 'IA', + 'theme.dark': 'Modo oscuro', 'theme.light': 'Modo claro', 'theme.toggle': 'Cambiar tema de color', + 'menu.open': 'Abrir menú', 'menu.close': 'Cerrar menú', 'nav.primary': 'Navegación principal', + 'language.label': 'Idioma', 'language.en': 'English', 'language.es': 'Español', + 'page.monitoring.title': 'Monitoreo de seguridad', 'page.monitoring.subtitle': 'Puntuación general y tendencias', + 'page.discovery.title': 'Descubrimiento de recursos', 'page.discovery.subtitle': 'Recursos del entorno de Azure', + 'page.prioritization.title': 'Priorización de riesgos', 'page.prioritization.subtitle': 'Qué corregir primero según riesgo y esfuerzo', + 'page.scan.title': 'Escaneo detallado', 'page.scan.subtitle': 'Hallazgos y guías de corrección', + 'page.compliance.title': 'Cumplimiento', 'page.compliance.subtitle': 'Controles y marcos de cumplimiento', + 'page.drift.title': 'Cambios de configuración', 'page.drift.subtitle': 'Cambios inesperados del entorno', + 'page.ai.title': 'Asistente de IA', 'page.ai.subtitle': 'Preguntas sobre la postura de seguridad', + 'scan.run': 'Ejecutar escaneo', 'scan.scanning': 'Escaneando… {seconds}s', 'scan.last': 'Último escaneo: {date}', + 'status.live': 'En línea', 'status.reconnecting': 'Reconectando', 'skip.content': 'Saltar al contenido principal', + }, +}; + +export function translate(locale, key, values = {}) { + const template = messages[locale]?.[key] ?? messages[DEFAULT_LOCALE][key] ?? key; + return Object.entries(values).reduce((text, [name, value]) => text.replaceAll(`{${name}}`, String(value)), template); +} diff --git a/frontend/src/i18n/messages.test.mjs b/frontend/src/i18n/messages.test.mjs new file mode 100644 index 0000000..5d09033 --- /dev/null +++ b/frontend/src/i18n/messages.test.mjs @@ -0,0 +1,11 @@ +import assert from 'node:assert/strict'; +import { DEFAULT_LOCALE, messages, translate } from './messages.js'; + +const referenceKeys = Object.keys(messages[DEFAULT_LOCALE]).sort(); +for (const [locale, catalog] of Object.entries(messages)) { + assert.deepEqual(Object.keys(catalog).sort(), referenceKeys, `${locale} must contain the complete message catalog`); +} +assert.equal(translate('es', 'nav.monitoring'), 'Monitorear'); +assert.equal(translate('unknown', 'nav.monitoring'), 'Monitor'); +assert.equal(translate('en', 'scan.scanning', { seconds: 12 }), 'Scanning… 12s'); +console.log('i18n catalogs valid'); diff --git a/frontend/src/pages/AILayer.jsx b/frontend/src/pages/AILayer.jsx index 63afa18..ed37840 100644 --- a/frontend/src/pages/AILayer.jsx +++ b/frontend/src/pages/AILayer.jsx @@ -98,13 +98,13 @@ export default function AILayer() { const [cveData, setCveData] = useState(null); const [cveLoading, setCveLoading] = useState(true); const [settingsKey, setSettingsKey] = useState(0); // force re-render on settings save + const initialFinding = location.state?.finding; useEffect(() => { api.getFindings() .then((scans) => { setFindings(scans); - const fromScan = location.state?.finding; - if (fromScan) setSelectedFinding(fromScan); + if (initialFinding) setSelectedFinding(initialFinding); aiApi.getSummary(scans).then(setSummary).finally(() => setSummaryLoading(false)); }) .catch(() => { @@ -112,7 +112,7 @@ export default function AILayer() { aiApi.getSummary([]).then(setSummary).finally(() => setSummaryLoading(false)); }); aiApi.getCVEAnalysis().then(setCveData).finally(() => setCveLoading(false)); - }, []); + }, [initialFinding]); const handleFindingSelect = (f) => setSelectedFinding((prev) => (prev?.id === f.id ? null : f)); diff --git a/frontend/src/pages/DetailedScan.jsx b/frontend/src/pages/DetailedScan.jsx index a2a7e66..41c8fee 100644 --- a/frontend/src/pages/DetailedScan.jsx +++ b/frontend/src/pages/DetailedScan.jsx @@ -81,18 +81,18 @@ export default function DetailedScan() { const [selected, setSelected] = useState(null); const [playbook, setPlaybook] = useState(null); const [showBanner, setShowBanner] = useState(!!location.state?.fromPrioritization); + const preSelectRuleId = location.state?.ruleId; // Load findings on mount useEffect(() => { api.getFindings().then((data) => { setFindings(data); - const preSelectRuleId = location.state?.ruleId; const initial = preSelectRuleId ? (data.find((f) => f.ruleId === preSelectRuleId) ?? data[0]) : data[0]; selectFinding(initial); }); - }, []); + }, [preSelectRuleId]); // Fetch playbook whenever selected finding changes async function selectFinding(f) {