diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..6cefb71f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: DevSecOps Baseline CI +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +# 1. CẤU HÌNH TRIGGER: Khi nào thì pipeline này được chạy? +on: + push: + branches: [ "main", "dev", "feature/docker_build" ] # Chạy khi có code push lên nhánh main hoặc dev + pull_request: + branches: [ "main" ] # Chạy khi có ai đó tạo Pull Request đòi gộp code vào main + +# 2. CẤU HÌNH JOBS: Các công việc cần làm +jobs: + # --- KIỂM TRA HẠ TẦNG --- + terraform-check: + name: 1. Terraform Validation + runs-on: ubuntu-latest + steps: + - name: 1. Checkout Source Code + uses: actions/checkout@v4 + + - name: 2. Setup Terraform + uses: hashicorp/setup-terraform@v3 + + - name: 3. Terraform Init & Validate + run: | + cd terraform + terraform init + terraform validate + + # # --- QUÉT MÃ NGUỒN BẢO MẬT (SAST) --- + sast-scan: + name: 2. SAST Scanning (Semgrep) + needs: terraform-check + runs-on: ubuntu-latest + steps: + - name: 1. Checkout Source Code + uses: actions/checkout@v4 + + - name: 2. Run Semgrep + uses: returntocorp/semgrep-action@v1 + with: + config: >- + p/security-audit + p/nodejs + + # --- DOCKER BUILD --- + build-and-test: + name: Build & Test Application + # needs: sast-scan + runs-on: ubuntu-latest # Mượn một máy ảo Ubuntu miễn phí của GitHub để chạy + + steps: + # Bước 1: Lấy code từ GitHub về cái máy ảo Ubuntu kia + - name: 1. Checkout Source Code + uses: actions/checkout@v4 + + # Bước 2: Setup môi trường + # --- Dành cho NodeJS --- + - name: 2. Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + # # --- Dành cho Python --- + # - name: 2. Setup Python + # uses: actions/setup-python@v4 + # with: + # python-version: '3.10' + + # Bước 3: Build thử Docker Image + - name: 3. Docker build image (No Push) + #pattern: ocker build -t ghcr.io/: thay repo_name thành tên mong muốn + run: docker build -t nt524:${{ github.sha }} . + + # Bước 4: Tạo SBOM với Syft + - name: 4. Generate SBOM with Syft + #install syft and using Syft to generate SBOM + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + syft nt524:${{ github.sha }} -o spdx-json > sbom.spdx.json + + # Bước 5: Quét Image với Trivy + - name: Run Trivy Vulnerability Scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'nt524:${{ github.sha }}' + format: 'table' + exit-code: '1' # Nếu Trivy phát hiện lỗ hỏng mức HIGH hoặc CRITICAl, nó sẽ trả về mã lỗi 1 + ignore-unfixed: true # Báo các lỗ hỏng đã có bản vá + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + diff --git a/.gitignore b/.gitignore index 4cd536b01..c5eb50001 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,7 @@ test/e2e/videos/ # ignore Snyk Code scanner files .dccache + +*.terraform +*.terraform.lock.hcl +*terraform.tfstate* diff --git a/.travis.yml b/.travis.yml index 21cc10c16..9d9c3e750 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ addons: packages: - libgconf-2-4 - + ## Cache NPM folder and Cypress binary ## to avoid downloading Cypress again and again cache: diff --git a/README.md b/README.md index 6b3754fd0..e69ba4de0 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Other settings can be changed by updating the [config file](https://github.com/O The repo includes the Dockerfile and docker-compose.yml necessary to set up the app and db instance, then connect them together. -1) Install [docker](https://docs.docker.com/installation/) and [docker compose](https://docs.docker.com/compose/install/) +1) Install [docker](https://docs.docker.com/installation/) and [docker compose](https://docs.docker.com/compose/install/) 2) Clone the github repository: ``` diff --git a/app/assets/vendor/theme/font-awesome/fonts/fontawesome-webfont.svg b/app/assets/vendor/theme/font-awesome/fonts/fontawesome-webfont.svg index 36d43b844..8185cf166 100644 --- a/app/assets/vendor/theme/font-awesome/fonts/fontawesome-webfont.svg +++ b/app/assets/vendor/theme/font-awesome/fonts/fontawesome-webfont.svg @@ -411,4 +411,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/data/allocations-dao.js b/app/data/allocations-dao.js index 24d4718c4..d0d37d0ee 100644 --- a/app/data/allocations-dao.js +++ b/app/data/allocations-dao.js @@ -66,9 +66,9 @@ const AllocationsDAO = function(db){ // to inject arbitrary javascript code into the NoSQL query: // 1. 0';while(true){}' // 2. 1'; return 1 == '1 - // Also implement fix in allocations.html for UX. + // Also implement fix in allocations.html for UX. const parsedThreshold = parseInt(threshold, 10); - + if (parsedThreshold >= 0 && parsedThreshold <= 99) { return {$where: `this.userId == ${parsedUserId} && this.stocks > ${parsedThreshold}`}; } diff --git a/app/routes/profile.js b/app/routes/profile.js index 0b5b34f2d..de5d99b1f 100644 --- a/app/routes/profile.js +++ b/app/routes/profile.js @@ -26,7 +26,7 @@ function ProfileHandler(db) { // doesn't end up as an XSS attack, the context is incorrect as it is encoding the firstname for HTML // while this same variable is also used in the context of a URL link element doc.website = ESAPI.encoder().encodeForHTML(doc.website); - // fix it by replacing the above with another template variable that is used for + // fix it by replacing the above with another template variable that is used for // the context of a URL in a link header // doc.website = ESAPI.encoder().encodeForURL(doc.website) diff --git a/app/views/allocations.html b/app/views/allocations.html index 3b28d546f..8016a9e5d 100644 --- a/app/views/allocations.html +++ b/app/views/allocations.html @@ -16,7 +16,7 @@

diff --git a/app/views/tutorial/a2.html b/app/views/tutorial/a2.html index 9202d8649..48dc2bbbc 100644 --- a/app/views/tutorial/a2.html +++ b/app/views/tutorial/a2.html @@ -236,15 +236,15 @@

Description

Implementing a robust minimum password criteria (minimum length and complexity) can make it difficult for attacker to guess password.
-
diff --git a/app/views/tutorial/a5.html b/app/views/tutorial/a5.html index 1bf72b5a2..a9fbf787d 100644 --- a/app/views/tutorial/a5.html +++ b/app/views/tutorial/a5.html @@ -87,8 +87,8 @@

Source Code Example

The default HTTP header x-powered-by can reveal implementation details to an attacker. It can be taken out by including this code in server.js -

   
-        app.disable("x-powered-by"); 
+                    
+        app.disable("x-powered-by");
     

The default session cookie name for express sessions can be changed by setting key attribute while creating express session. diff --git a/app/views/tutorial/a8.html b/app/views/tutorial/a8.html index ae1883bdf..ebb7205a2 100644 --- a/app/views/tutorial/a8.html +++ b/app/views/tutorial/a8.html @@ -89,9 +89,9 @@

Source Code Example

//Enable Express csrf protection app.use(express.csrf()); - app.use(function(req, res, next) { - res.locals.csrftoken = req.csrfToken(); - next(); + app.use(function(req, res, next) { + res.locals.csrftoken = req.csrfToken(); + next(); });
Next, this token can be included in a hidden form field in views/profile.htmlas below.
diff --git a/app/views/tutorial/ssrf.html b/app/views/tutorial/ssrf.html
index 11caba305..7103b38e5 100644
--- a/app/views/tutorial/ssrf.html
+++ b/app/views/tutorial/ssrf.html
@@ -26,7 +26,7 @@ 

Attack Mechanics

     // If a stock symbol has been submitted, concatenate the symbol to the URL and return the HTTP Response
     if (req.query.symbol) {
-        var url = req.query.url+req.query.symbol; 
+        var url = req.query.url+req.query.symbol;
         needle.get(url, function(error, newResponse) { ... }
                     
An attacker can change the url and symbol parameters to point to an attacker-controlled website to interact with the server. diff --git a/docker-compose.yml b/docker-compose.yml index 49fb28f81..2c78c99b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.7" +# version: "3.7" services: web: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..0f1f47823 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,13 @@ +const securityPlugin = require("eslint-plugin-security"); + +module.exports = [ + // Bật toàn bộ rule bảo mật khuyên dùng + securityPlugin.configs.recommended, + { + // Cấu hình môi trường cho NodeGoat (Node.js) + languageOptions: { + ecmaVersion: "latest", + sourceType: "commonjs" + } + } +]; \ No newline at end of file diff --git a/k8s/mongodb_deployment.yaml b/k8s/mongodb_deployment.yaml new file mode 100644 index 000000000..93db9fdea --- /dev/null +++ b/k8s/mongodb_deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongodb + namespace: nodegoat-staging +spec: + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: mongo:4.4 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 27017 diff --git a/k8s/mongodb_service.yaml b/k8s/mongodb_service.yaml new file mode 100644 index 000000000..4c0942f14 --- /dev/null +++ b/k8s/mongodb_service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongodb + namespace: nodegoat-staging +spec: + selector: + app: mongodb + ports: + - protocol: TCP + port: 27017 + targetPort: 27017 diff --git a/k8s/nodegoat_deployment.yaml b/k8s/nodegoat_deployment.yaml new file mode 100644 index 000000000..d25281ae3 --- /dev/null +++ b/k8s/nodegoat_deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nodegoat-app + namespace: nodegoat-staging +spec: + replicas: 1 + selector: + matchLabels: + app: nodegoat + template: + metadata: + labels: + app: nodegoat + spec: + containers: + - name: nodegoat + # Thay thế bằng image bạn đã build ở Bước 1 + image: ${app_image} #thay bằng tên image vừa build + imagePullPolicy: IfNotPresent + ports: + - containerPort: 4000 + env: + # Cấu hình chuỗi kết nối DB trỏ tới Service MongoDB nội bộ + - name: MONGODB_URI + value: "mongodb://mongodb:27017/nodegoat" + command: [ "sh", "-c" ] + args: + - "until nc -z -w 2 mongodb 27017 && echo 'mongodb is ready for connections' && node artifacts/db-reset.js && npm start; do sleep 2; done" diff --git a/k8s/nodegoat_service.yaml b/k8s/nodegoat_service.yaml new file mode 100644 index 000000000..42ac28a1b --- /dev/null +++ b/k8s/nodegoat_service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: nodegoat-app + namespace: nodegoat-staging +spec: + type: ClusterIP + selector: + app: nodegoat + ports: + - protocol: TCP + port: 4000 + targetPort: 4000 diff --git a/terraform/Namespace.tf b/terraform/Namespace.tf new file mode 100644 index 000000000..9c8d3c23d --- /dev/null +++ b/terraform/Namespace.tf @@ -0,0 +1,11 @@ +# Create a Kubernetes namespace for the NodeGoat staging environment +resource "kubernetes_namespace" "nodegoat_staging" { + metadata { + name = "nodegoat-staging" + labels ={ + environment = "staging" + app = "nodegoat" + "pod-security.kubernetes.io/enforce" = "baseline" + } + } +} \ No newline at end of file diff --git a/terraform/Network_Policy.tf b/terraform/Network_Policy.tf new file mode 100644 index 000000000..450f11831 --- /dev/null +++ b/terraform/Network_Policy.tf @@ -0,0 +1,18 @@ +resource "kubernetes_network_policy" "staging_internal_allow" { + metadata { + name = "allow-internal-staging" + namespace = "nodegoat-staging" + } + + spec { + pod_selector {} # Áp dụng cho TẤT CẢ pod trong namespace + + ingress { + from { + pod_selector {} # Cho phép traffic đến từ các pod khác CÙNG namespace + } + } + + policy_types = ["Ingress"] + } +} \ No newline at end of file diff --git a/terraform/provider.tf b/terraform/provider.tf new file mode 100644 index 000000000..9b6064db3 --- /dev/null +++ b/terraform/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.23.0" + } + } +} + +provider "kubernetes" { + config_path = "~/.kube/config" +}