tum 4 周之前
父節點
當前提交
1d73c93084

+ 6 - 0
flask-openapi-shuffle/.dockerignore

@@ -0,0 +1,6 @@
1
+__pycache__/
2
+*.pyc
3
+.pytest_cache/
4
+.venv/
5
+venv/
6
+.git/

+ 16 - 0
flask-openapi-shuffle/Dockerfile

@@ -0,0 +1,16 @@
1
+FROM python:3.11-slim
2
+
3
+ENV PYTHONDONTWRITEBYTECODE=1 \
4
+    PYTHONUNBUFFERED=1
5
+
6
+WORKDIR /service
7
+
8
+COPY requirements.txt .
9
+RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+COPY app ./app
12
+COPY app.py .
13
+
14
+EXPOSE 8000
15
+
16
+CMD ["python", "app.py"]

+ 34 - 0
flask-openapi-shuffle/README.md

@@ -0,0 +1,34 @@
1
+# Flask OpenAPI Demo for Shuffle
2
+
3
+This project is an OpenAPI-first Flask service using Connexion.
4
+
5
+## Endpoints
6
+
7
+- `GET /health`
8
+- `POST /echo`
9
+- `GET /ioc/{value}`
10
+- Swagger UI: `GET /ui`
11
+- OpenAPI spec: `GET /openapi.json`
12
+
13
+## Run with Docker
14
+
15
+```bash
16
+cd /Users/simplicoltd./projects/soc/flask-openapi-shuffle
17
+docker compose up --build
18
+```
19
+
20
+Test quickly:
21
+
22
+```bash
23
+curl -s http://localhost:8000/health
24
+curl -s -X POST http://localhost:8000/echo -H 'Content-Type: application/json' -d '{"message":"hello"}'
25
+curl -s http://localhost:8000/ioc/8.8.8.8
26
+```
27
+
28
+## Use with Shuffle
29
+
30
+In Shuffle App Creator / OpenAPI import, use one of these URLs:
31
+
32
+- `http://localhost:8000/openapi.json` (if Shuffle can reach your host localhost)
33
+- `http://host.docker.internal:8000/openapi.json` (common when Shuffle runs in Docker on Mac/Windows)
34
+- `http://<your-host-ip>:8000/openapi.json` (Linux Docker or remote host)

+ 19 - 0
flask-openapi-shuffle/app.py

@@ -0,0 +1,19 @@
1
+import connexion
2
+
3
+
4
+def create_app():
5
+    cnx_app = connexion.FlaskApp(
6
+        __name__,
7
+        specification_dir="app",
8
+    )
9
+    cnx_app.add_api(
10
+        "openapi.yaml",
11
+        strict_validation=True,
12
+        validate_responses=True,
13
+    )
14
+    return cnx_app
15
+
16
+
17
+if __name__ == "__main__":
18
+    app = create_app()
19
+    app.run(host="0.0.0.0", port=8000)

+ 1 - 0
flask-openapi-shuffle/app/__init__.py

@@ -0,0 +1 @@
1
+# Package marker for Connexion operation imports.

+ 27 - 0
flask-openapi-shuffle/app/handlers.py

@@ -0,0 +1,27 @@
1
+from datetime import datetime, timezone
2
+
3
+
4
+def health_check():
5
+    return {"status": "ok", "timestamp": datetime.now(timezone.utc).isoformat()}, 200
6
+
7
+
8
+def echo_body(body):
9
+    return {"received": body, "length": len(str(body))}, 200
10
+
11
+
12
+def lookup_ioc(value, ioc_type="auto"):
13
+    normalized = value.strip().lower()
14
+    verdict = "unknown"
15
+    if "." in normalized and " " not in normalized:
16
+        verdict = "domain-like"
17
+    if normalized.count(".") == 3 and all(part.isdigit() for part in normalized.split(".")):
18
+        verdict = "ipv4-like"
19
+    if len(normalized) in {32, 40, 64} and all(c in "0123456789abcdef" for c in normalized):
20
+        verdict = "hash-like"
21
+
22
+    return {
23
+        "input": value,
24
+        "type": ioc_type,
25
+        "verdict": verdict,
26
+        "source": "demo-local",
27
+    }, 200

+ 106 - 0
flask-openapi-shuffle/app/openapi.yaml

@@ -0,0 +1,106 @@
1
+openapi: 3.0.3
2
+info:
3
+  title: SOC Demo Flask API
4
+  version: 1.0.0
5
+  description: OpenAPI-first Flask service for Shuffle testing.
6
+servers:
7
+  - url: http://localhost:8000
8
+paths:
9
+  /health:
10
+    get:
11
+      operationId: app.handlers.health_check
12
+      summary: Health check
13
+      responses:
14
+        "200":
15
+          description: Service is healthy
16
+          content:
17
+            application/json:
18
+              schema:
19
+                $ref: "#/components/schemas/HealthResponse"
20
+  /echo:
21
+    post:
22
+      operationId: app.handlers.echo_body
23
+      summary: Echo request body
24
+      requestBody:
25
+        required: true
26
+        content:
27
+          application/json:
28
+            schema:
29
+              $ref: "#/components/schemas/EchoRequest"
30
+      responses:
31
+        "200":
32
+          description: Echo response
33
+          content:
34
+            application/json:
35
+              schema:
36
+                $ref: "#/components/schemas/EchoResponse"
37
+  /ioc/{value}:
38
+    get:
39
+      operationId: app.handlers.lookup_ioc
40
+      summary: Basic IOC classification demo
41
+      parameters:
42
+        - name: value
43
+          in: path
44
+          required: true
45
+          schema:
46
+            type: string
47
+        - name: ioc_type
48
+          in: query
49
+          required: false
50
+          schema:
51
+            type: string
52
+            default: auto
53
+      responses:
54
+        "200":
55
+          description: IOC lookup result
56
+          content:
57
+            application/json:
58
+              schema:
59
+                $ref: "#/components/schemas/LookupResponse"
60
+components:
61
+  schemas:
62
+    HealthResponse:
63
+      type: object
64
+      properties:
65
+        status:
66
+          type: string
67
+          example: ok
68
+        timestamp:
69
+          type: string
70
+          format: date-time
71
+      required:
72
+        - status
73
+        - timestamp
74
+    EchoRequest:
75
+      type: object
76
+      additionalProperties: true
77
+      example:
78
+        message: test from Shuffle
79
+    EchoResponse:
80
+      type: object
81
+      properties:
82
+        received:
83
+          type: object
84
+          additionalProperties: true
85
+        length:
86
+          type: integer
87
+      required:
88
+        - received
89
+        - length
90
+    LookupResponse:
91
+      type: object
92
+      properties:
93
+        input:
94
+          type: string
95
+        type:
96
+          type: string
97
+        verdict:
98
+          type: string
99
+          example: domain-like
100
+        source:
101
+          type: string
102
+      required:
103
+        - input
104
+        - type
105
+        - verdict
106
+        - source

+ 6 - 0
flask-openapi-shuffle/docker-compose.yml

@@ -0,0 +1,6 @@
1
+services:
2
+  flask-openapi:
3
+    build: .
4
+    container_name: flask-openapi-shuffle
5
+    ports:
6
+      - "8000:8000"

+ 3 - 0
flask-openapi-shuffle/requirements.txt

@@ -0,0 +1,3 @@
1
+connexion[flask,swagger-ui]==3.1.0
2
+flask==3.1.0
3
+uvicorn==0.34.0

+ 38 - 9
run-combined-stack.sh

@@ -26,24 +26,27 @@ Usage:
26 26
 
27 27
 Commands:
28 28
   up           Start services (default: up -d when no args)
29
-  down         Stop services
29
+  down         Stop services (requires explicit target)
30 30
   logs         View logs
31 31
   status       Show container and endpoint status
32 32
   help         Show this help message
33 33
 
34 34
 Targets:
35
-  all|--all    All stacks (wazuh, iris, shuffle, pagerduty, integrator)
35
+  all|--all    All stacks (wazuh, iris, shuffle, pagerduty, integrator, flask-openapi-shuffle)
36 36
   wazuh        Wazuh single-node stack
37 37
   iris         IRIS-web stack
38 38
   shuffle      Shuffle stack
39 39
   pagerduty    PagerDuty stub
40 40
   integrator   SOC integrator stack
41
+  flask-openapi-shuffle  Flask OpenAPI demo stack
41 42
 
42 43
 Examples:
43 44
   ./run-combined-stack.sh
44 45
   ./run-combined-stack.sh up --all -d
45 46
   ./run-combined-stack.sh up iris -d
47
+  ./run-combined-stack.sh up flask-openapi-shuffle -d
46 48
   ./run-combined-stack.sh down shuffle
49
+  ./run-combined-stack.sh down --all
47 50
   ./run-combined-stack.sh logs integrator -f
48 51
   ./run-combined-stack.sh logs --all --tail 200
49 52
   ./run-combined-stack.sh status
@@ -104,6 +107,14 @@ run_soc_integrator() {
104 107
     "${COMMAND}" ${ARGS[@]+"${ARGS[@]}"}
105 108
 }
106 109
 
110
+run_flask_openapi_shuffle() {
111
+  docker compose \
112
+    --project-name flask-openapi-shuffle \
113
+    --project-directory "${ROOT_DIR}/flask-openapi-shuffle" \
114
+    -f "${ROOT_DIR}/flask-openapi-shuffle/docker-compose.yml" \
115
+    "${COMMAND}" ${ARGS[@]+"${ARGS[@]}"}
116
+}
117
+
107 118
 run_target() {
108 119
   local target="$1"
109 120
   case "${target}" in
@@ -122,9 +133,12 @@ run_target() {
122 133
     integrator)
123 134
       run_soc_integrator
124 135
       ;;
136
+    flask-openapi-shuffle)
137
+      run_flask_openapi_shuffle
138
+      ;;
125 139
     *)
126 140
       echo "Unknown target: ${target}"
127
-      echo "Use one of: wazuh, iris, shuffle, pagerduty, integrator"
141
+      echo "Use one of: wazuh, iris, shuffle, pagerduty, integrator, flask-openapi-shuffle"
128 142
       exit 1
129 143
       ;;
130 144
   esac
@@ -134,6 +148,7 @@ run_all() {
134 148
   local mode="$1"
135 149
 
136 150
   if [[ "${mode}" == "down" ]]; then
151
+    run_flask_openapi_shuffle
137 152
     run_soc_integrator
138 153
     run_pagerduty_stub
139 154
     run_shuffle
@@ -145,6 +160,7 @@ run_all() {
145 160
     run_shuffle
146 161
     run_pagerduty_stub
147 162
     run_soc_integrator
163
+    run_flask_openapi_shuffle
148 164
   fi
149 165
 }
150 166
 
@@ -157,6 +173,7 @@ follow_all_logs() {
157 173
   run_shuffle &
158 174
   run_pagerduty_stub &
159 175
   run_soc_integrator &
176
+  run_flask_openapi_shuffle &
160 177
 
161 178
   trap 'kill 0' INT TERM
162 179
   wait
@@ -180,25 +197,37 @@ run_logs_for_target() {
180 197
     integrator)
181 198
       run_soc_integrator
182 199
       ;;
200
+    flask-openapi-shuffle)
201
+      run_flask_openapi_shuffle
202
+      ;;
183 203
     all|--all)
184 204
       run_wazuh
185 205
       run_iris
186 206
       run_shuffle
187 207
       run_pagerduty_stub
188 208
       run_soc_integrator
209
+      run_flask_openapi_shuffle
189 210
       ;;
190 211
     *)
191 212
       echo "Unknown logs target: ${target}"
192
-      echo "Use one of: wazuh, iris, shuffle, pagerduty, integrator"
213
+      echo "Use one of: wazuh, iris, shuffle, pagerduty, integrator, flask-openapi-shuffle"
193 214
       exit 1
194 215
       ;;
195 216
   esac
196 217
 }
197 218
 
198 219
 if [[ "${COMMAND}" == "down" ]]; then
199
-  TARGET="${1:-all}"
220
+  TARGET="${1:-}"
221
+  if [[ -z "${TARGET}" ]]; then
222
+    echo "Refusing to run 'down' without a target."
223
+    echo "Use one of:"
224
+    echo "  ./run-combined-stack.sh down <wazuh|iris|shuffle|pagerduty|integrator|flask-openapi-shuffle>"
225
+    echo "  ./run-combined-stack.sh down --all"
226
+    exit 1
227
+  fi
228
+
200 229
   case "${TARGET}" in
201
-    wazuh|iris|shuffle|pagerduty|integrator)
230
+    wazuh|iris|shuffle|pagerduty|integrator|flask-openapi-shuffle)
202 231
       ARGS=("${ARGS[@]:1}")
203 232
       run_target "${TARGET}"
204 233
       ;;
@@ -213,7 +242,7 @@ if [[ "${COMMAND}" == "down" ]]; then
213 242
 elif [[ "${COMMAND}" == "logs" ]]; then
214 243
   LOGS_TARGET="${1:-all}"
215 244
   case "${LOGS_TARGET}" in
216
-    wazuh|iris|shuffle|pagerduty|integrator)
245
+    wazuh|iris|shuffle|pagerduty|integrator|flask-openapi-shuffle)
217 246
       ARGS=("${ARGS[@]:1}")
218 247
       run_logs_for_target "${LOGS_TARGET}"
219 248
       ;;
@@ -225,7 +254,7 @@ elif [[ "${COMMAND}" == "logs" ]]; then
225 254
       for arg in ${ARGS[@]+"${ARGS[@]}"}; do
226 255
         if [[ "${arg}" == "-f" || "${arg}" == "--follow" ]]; then
227 256
           echo "For follow mode, specify one target:"
228
-          echo "./run-combined-stack.sh logs <wazuh|iris|shuffle|pagerduty|integrator> -f"
257
+          echo "./run-combined-stack.sh logs <wazuh|iris|shuffle|pagerduty|integrator|flask-openapi-shuffle> -f"
229 258
           exit 1
230 259
         fi
231 260
       done
@@ -235,7 +264,7 @@ elif [[ "${COMMAND}" == "logs" ]]; then
235 264
 elif [[ "${COMMAND}" == "up" ]]; then
236 265
   TARGET="${1:-all}"
237 266
   case "${TARGET}" in
238
-    wazuh|iris|shuffle|pagerduty|integrator)
267
+    wazuh|iris|shuffle|pagerduty|integrator|flask-openapi-shuffle)
239 268
       ARGS=("${ARGS[@]:1}")
240 269
       run_target "${TARGET}"
241 270
       ;;

+ 244 - 0
shuffle-workflows/sample-ip-ioc-check-workflow.json

@@ -0,0 +1,244 @@
1
+{
2
+  "workflow_as_code": false,
3
+  "actions": [
4
+    {
5
+      "app_name": "Shuffle Tools",
6
+      "app_version": "1.2.0",
7
+      "description": "Receive IP input from webhook payload.",
8
+      "app_id": "0671c57b-3af6-43f7-9501-b2f916c127c8",
9
+      "errors": [],
10
+      "id": "b6f4c2a8-3d6d-4d1f-8c70-03d09a1e6f11",
11
+      "is_valid": true,
12
+      "isStartNode": true,
13
+      "sharing": true,
14
+      "label": "Webhook Trigger (IP Input)",
15
+      "public": true,
16
+      "generated": false,
17
+      "large_image": "",
18
+      "environment": "Shuffle",
19
+      "name": "webhook",
20
+      "parameters": [
21
+        {
22
+          "name": "source_ip",
23
+          "value": "",
24
+          "description": "IP address to check as IOC.",
25
+          "required": true,
26
+          "multiline": false,
27
+          "multiselect": false,
28
+          "options": null,
29
+          "action_field": "",
30
+          "variant": "",
31
+          "configuration": false,
32
+          "tags": null,
33
+          "schema": {
34
+            "type": ""
35
+          },
36
+          "skip_multicheck": false,
37
+          "value_replace": null,
38
+          "unique_toggled": false,
39
+          "error": "",
40
+          "hidden": false
41
+        }
42
+      ],
43
+      "execution_variable": {
44
+        "description": "",
45
+        "id": "",
46
+        "name": "",
47
+        "value": ""
48
+      },
49
+      "position": {
50
+        "x": 100,
51
+        "y": 120
52
+      },
53
+      "authentication_id": "",
54
+      "category": "",
55
+      "reference_url": "",
56
+      "sub_action": false,
57
+      "run_magic_output": false,
58
+      "run_magic_input": false,
59
+      "execution_delay": 0,
60
+      "category_label": null,
61
+      "suggestion": false,
62
+      "parent_controlled": false,
63
+      "source_workflow": "",
64
+      "source_execution": ""
65
+    },
66
+    {
67
+      "app_name": "Shuffle Tools",
68
+      "app_version": "1.2.0",
69
+      "description": "Build request body for SOC IOC evaluation endpoint.",
70
+      "app_id": "0671c57b-3af6-43f7-9501-b2f916c127c8",
71
+      "errors": [],
72
+      "id": "e72dcb76-a265-4da1-a0dd-2f65f558b52f",
73
+      "is_valid": true,
74
+      "isStartNode": false,
75
+      "sharing": true,
76
+      "label": "Prepare IOC Check Payload",
77
+      "public": true,
78
+      "generated": false,
79
+      "large_image": "",
80
+      "environment": "Shuffle",
81
+      "name": "repeat_back_to_me",
82
+      "parameters": [
83
+        {
84
+          "name": "call",
85
+          "value": "{\"ioc_type\":\"ip\",\"ioc_value\":\"{{actions.b6f4c2a8-3d6d-4d1f-8c70-03d09a1e6f11.source_ip}}\",\"source_event\":{\"event_id\":\"shuffle-ip-check-sample\",\"network\":{\"src_ip\":\"{{actions.b6f4c2a8-3d6d-4d1f-8c70-03d09a1e6f11.source_ip}}\"}}}",
86
+          "description": "Use this JSON as body for POST /mvp/ioc/evaluate in a HTTP node.",
87
+          "required": false,
88
+          "multiline": true,
89
+          "multiselect": false,
90
+          "options": null,
91
+          "action_field": "",
92
+          "variant": "",
93
+          "configuration": false,
94
+          "tags": null,
95
+          "schema": {
96
+            "type": ""
97
+          },
98
+          "skip_multicheck": false,
99
+          "value_replace": null,
100
+          "unique_toggled": false,
101
+          "error": "",
102
+          "hidden": false
103
+        }
104
+      ],
105
+      "execution_variable": {
106
+        "description": "",
107
+        "id": "",
108
+        "name": "",
109
+        "value": ""
110
+      },
111
+      "position": {
112
+        "x": 430,
113
+        "y": 120
114
+      },
115
+      "authentication_id": "",
116
+      "category": "",
117
+      "reference_url": "",
118
+      "sub_action": false,
119
+      "run_magic_output": false,
120
+      "run_magic_input": false,
121
+      "execution_delay": 0,
122
+      "category_label": null,
123
+      "suggestion": false,
124
+      "parent_controlled": false,
125
+      "source_workflow": "",
126
+      "source_execution": ""
127
+    }
128
+  ],
129
+  "branches": [
130
+    {
131
+      "id": "branch-ip-ioc-1",
132
+      "source": "b6f4c2a8-3d6d-4d1f-8c70-03d09a1e6f11",
133
+      "destination": "e72dcb76-a265-4da1-a0dd-2f65f558b52f",
134
+      "success": true,
135
+      "label": ""
136
+    }
137
+  ],
138
+  "visual_branches": null,
139
+  "triggers": [],
140
+  "comments": [],
141
+  "configuration": {
142
+    "exit_on_error": false,
143
+    "start_from_top": false,
144
+    "skip_notifications": false
145
+  },
146
+  "created": 1771470000,
147
+  "edited": 1771470000,
148
+  "last_runtime": 0,
149
+  "due_date": 0,
150
+  "id": "d2ccf0bd-bf4d-4f77-8eea-c1a65f1ea3e9",
151
+  "is_valid": true,
152
+  "name": "Sample - IP IOC Check Payload Builder",
153
+  "description": "Sample Shuffle workflow JSON for IP IOC check integration. Trigger with source_ip, then pass generated JSON to HTTP POST /mvp/ioc/evaluate.",
154
+  "start": "b6f4c2a8-3d6d-4d1f-8c70-03d09a1e6f11",
155
+  "owner": "1050bd5b-b1bb-4c22-acfb-94156cdc0567",
156
+  "sharing": "private",
157
+  "execution_org": {
158
+    "name": "default",
159
+    "id": "03264040-f718-4a61-b9ac-61c7cac3fe99",
160
+    "users": [],
161
+    "role": "admin",
162
+    "child_orgs": null,
163
+    "region_url": "",
164
+    "is_partner": false,
165
+    "image": "",
166
+    "creator_org": "",
167
+    "branding": {
168
+      "enable_chat": false,
169
+      "home_url": "",
170
+      "theme": "",
171
+      "documentation_link": "",
172
+      "global_user": false,
173
+      "support_email": "",
174
+      "logout_url": "",
175
+      "brand_color": "",
176
+      "brand_name": ""
177
+    }
178
+  },
179
+  "org_id": "03264040-f718-4a61-b9ac-61c7cac3fe99",
180
+  "workflow_variables": null,
181
+  "execution_environment": "",
182
+  "previously_saved": true,
183
+  "categories": {
184
+    "intel": {
185
+      "name": "intel",
186
+      "count": 0,
187
+      "id": "",
188
+      "description": "",
189
+      "large_image": ""
190
+    }
191
+  },
192
+  "example_argument": "",
193
+  "public": false,
194
+  "default_return_value": "",
195
+  "contact_info": {
196
+    "name": "",
197
+    "url": ""
198
+  },
199
+  "published_id": "",
200
+  "revision_id": "",
201
+  "usecase_ids": null,
202
+  "input_questions": null,
203
+  "form_control": {
204
+    "input_markdown": "",
205
+    "output_yields": null,
206
+    "cleanup_actions": null,
207
+    "form_width": 0
208
+  },
209
+  "blogpost": "",
210
+  "video": "",
211
+  "status": "test",
212
+  "workflow_type": "",
213
+  "generated": false,
214
+  "hidden": false,
215
+  "background_processing": false,
216
+  "updated_by": "root",
217
+  "validated": false,
218
+  "validation": {
219
+    "valid": false,
220
+    "changed_at": 0,
221
+    "last_valid": 0,
222
+    "validation_ran": false,
223
+    "notifications_created": 0,
224
+    "environment": "",
225
+    "workflow_id": "",
226
+    "execution_id": "",
227
+    "node_id": "",
228
+    "total_problems": 0,
229
+    "errors": [],
230
+    "subflow_apps": []
231
+  },
232
+  "parentorg_workflow": "",
233
+  "childorg_workflow_ids": null,
234
+  "suborg_distribution": [],
235
+  "backup_config": {
236
+    "onprem_backup": false,
237
+    "upload_repo": "",
238
+    "upload_branch": "",
239
+    "upload_username": "",
240
+    "upload_token": "",
241
+    "tokens_encrypted": false
242
+  },
243
+  "auth_groups": null
244
+}

File diff suppressed because it is too large
+ 553 - 0
virustotal_v3.json


File diff suppressed because it is too large
+ 1 - 0
virustotal_v3.openapi.json


File diff suppressed because it is too large
+ 1 - 0
virustotal_v3.yaml