Skip to content

Commit 0390885

Browse files
[Netskope] Add multiple system tests for Alerts_v2 and Events_v2 data stream (#14887)
netskope: add multiple system tests for alerts_v2 and events_v2 data streams
1 parent ed19772 commit 0390885

File tree

34 files changed

+1073
-157
lines changed

34 files changed

+1073
-157
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
version: "2.3"
2+
services:
3+
azure-blob-storage-emulator:
4+
image: mcr.microsoft.com/azure-storage/azurite
5+
command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000
6+
ports:
7+
- "10000/tcp"
8+
uploader:
9+
image: mcr.microsoft.com/azure-cli
10+
depends_on:
11+
- azure-blob-storage-emulator
12+
volumes:
13+
- ./sample_logs:/sample_logs
14+
entrypoint: >
15+
sh -c "
16+
sleep 5 &&
17+
export AZURE_STORAGE_CONNECTION_STRING='DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azure-blob-storage-emulator:10000/devstoreaccount1;' &&
18+
az storage container create --name test-container &&
19+
az storage blob upload --container-name test-container --file /sample_logs/test-alerts-v2.csv.gz --name test-alerts-v2.csv.gz
20+
"
21+
gcs-mock-service:
22+
image: golang:1.24.7-alpine
23+
working_dir: /app
24+
volumes:
25+
- ./gcs-mock-service:/app
26+
- ./files/manifest.yml:/files/manifest.yml:ro
27+
- ./sample_logs/:/data
28+
ports:
29+
- "4443/tcp"
30+
healthcheck:
31+
test: "wget --no-verbose --tries=1 --spider http://localhost:4443/health || exit 1"
32+
interval: 10s
33+
timeout: 5s
34+
command: go run main.go -manifest /files/manifest.yml
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
buckets:
2+
testbucket:
3+
files:
4+
- path: /data/test-alerts-v2.csv.gz
5+
content-type: application/x-gzip
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module gcs-mock-service
2+
3+
go 1.24.7
4+
5+
require gopkg.in/yaml.v3 v3.0.1
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package main
6+
7+
import (
8+
"encoding/json"
9+
"flag"
10+
"fmt"
11+
"io"
12+
"log"
13+
"net/http"
14+
"os"
15+
"strconv"
16+
"strings"
17+
18+
"gopkg.in/yaml.v3"
19+
)
20+
21+
func main() {
22+
host := flag.String("host", "0.0.0.0", "host to listen on")
23+
port := flag.String("port", "4443", "port to listen on")
24+
manifest := flag.String("manifest", "", "path to YAML manifest file for preloading buckets and objects")
25+
flag.Parse()
26+
27+
addr := fmt.Sprintf("%s:%s", *host, *port)
28+
29+
fmt.Printf("Starting mock GCS server on http://%s\n", addr)
30+
if *manifest != "" {
31+
m, err := readManifest(*manifest)
32+
if err != nil {
33+
log.Fatalf("error reading manifest: %v", err)
34+
}
35+
if err := processManifest(m); err != nil {
36+
log.Fatalf("error processing manifest: %v", err)
37+
}
38+
} else {
39+
fmt.Println("Store is empty. Create buckets and objects via API calls.")
40+
}
41+
42+
// setup HTTP handlers
43+
mux := http.NewServeMux()
44+
// health check
45+
mux.HandleFunc("/health", healthHandler)
46+
// standard gcs api calls
47+
mux.HandleFunc("GET /storage/v1/b/{bucket}/o", handleListObjects)
48+
mux.HandleFunc("GET /storage/v1/b/{bucket}/o/{object...}", handleGetObject)
49+
mux.HandleFunc("POST /storage/v1/b", handleCreateBucket)
50+
mux.HandleFunc("POST /upload/storage/v1/b/{bucket}/o", handleUploadObject)
51+
mux.HandleFunc("POST /upload/storage/v1/b/{bucket}/o/{object...}", handleUploadObject)
52+
// direct path-style gcs sdk calls
53+
mux.HandleFunc("GET /{bucket}/o/{object...}", handleGetObject)
54+
mux.HandleFunc("GET /{bucket}/{object...}", handleGetObject)
55+
// debug: log all requests
56+
loggedMux := loggingMiddleware(mux)
57+
58+
if err := http.ListenAndServe(addr, loggedMux); err != nil {
59+
log.Fatalf("failed to start server: %v", err)
60+
}
61+
}
62+
63+
// loggingMiddleware logs incoming HTTP requests.
64+
func loggingMiddleware(next http.Handler) http.Handler {
65+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66+
fmt.Printf("%s %s\n", r.Method, r.URL.Path)
67+
next.ServeHTTP(w, r)
68+
})
69+
}
70+
71+
// readManifest reads and parses the YAML manifest file.
72+
func readManifest(path string) (*Manifest, error) {
73+
data, err := os.ReadFile(path)
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to read manifest: %w", err)
76+
}
77+
78+
var manifest Manifest
79+
if err := yaml.Unmarshal(data, &manifest); err != nil {
80+
return nil, fmt.Errorf("failed to parse manifest: %w", err)
81+
}
82+
83+
return &manifest, nil
84+
}
85+
86+
// processManifest creates buckets and uploads objects as specified in the manifest.
87+
func processManifest(manifest *Manifest) error {
88+
for bucketName, bucket := range manifest.Buckets {
89+
for _, file := range bucket.Files {
90+
fmt.Printf("preloading data for bucket: %s | path: %s | content-type: %s...\n",
91+
bucketName, file.Path, file.ContentType)
92+
93+
if err := createBucket(bucketName); err != nil {
94+
return fmt.Errorf("failed to create bucket '%s': %w", bucketName, err)
95+
}
96+
data, err := os.ReadFile(file.Path)
97+
if err != nil {
98+
return fmt.Errorf("failed to read bucket data file '%s': %w", file.Path, err)
99+
}
100+
pathParts := strings.Split(file.Path, "/")
101+
if _, err := uploadObject(bucketName, pathParts[len(pathParts)-1], data, file.ContentType); err != nil {
102+
return fmt.Errorf("failed to create object '%s' in bucket '%s': %w", file.Path, bucketName, err)
103+
}
104+
}
105+
}
106+
return nil
107+
}
108+
109+
// healthHandler responds with a simple "OK" message for health checks.
110+
func healthHandler(w http.ResponseWriter, r *http.Request) {
111+
w.WriteHeader(http.StatusOK)
112+
fmt.Fprint(w, "OK")
113+
}
114+
115+
// handleListObjects lists all objects in the specified bucket.
116+
func handleListObjects(w http.ResponseWriter, r *http.Request) {
117+
bucketName := r.PathValue("bucket")
118+
119+
if bucket, ok := inMemoryStore[bucketName]; ok {
120+
response := GCSListResponse{
121+
Kind: "storage#objects",
122+
Items: []GCSObject{},
123+
}
124+
for name, object := range bucket {
125+
item := GCSObject{
126+
Kind: "storage#object",
127+
Name: name,
128+
Bucket: bucketName,
129+
Size: strconv.Itoa(len(object.Data)),
130+
ContentType: object.ContentType,
131+
}
132+
response.Items = append(response.Items, item)
133+
}
134+
w.Header().Set("Content-Type", "application/json")
135+
json.NewEncoder(w).Encode(response)
136+
return
137+
}
138+
http.Error(w, "not found", http.StatusNotFound)
139+
}
140+
141+
// handleGetObject retrieves a specific object from a bucket.
142+
func handleGetObject(w http.ResponseWriter, r *http.Request) {
143+
bucketName := r.PathValue("bucket")
144+
objectName := r.PathValue("object")
145+
146+
if bucketName == "" || objectName == "" {
147+
http.Error(w, "not found: invalid URL format", http.StatusNotFound)
148+
return
149+
}
150+
151+
if bucket, ok := inMemoryStore[bucketName]; ok {
152+
if object, ok := bucket[objectName]; ok {
153+
w.Header().Set("Content-Type", object.ContentType)
154+
w.Write(object.Data)
155+
return
156+
}
157+
}
158+
http.Error(w, "not found", http.StatusNotFound)
159+
}
160+
161+
// handleCreateBucket creates a new bucket.
162+
func handleCreateBucket(w http.ResponseWriter, r *http.Request) {
163+
if r.Method != http.MethodPost {
164+
http.NotFound(w, r)
165+
return
166+
}
167+
168+
var bucketInfo struct {
169+
Name string `json:"name"`
170+
}
171+
if err := json.NewDecoder(r.Body).Decode(&bucketInfo); err != nil {
172+
http.Error(w, "invalid JSON body", http.StatusBadRequest)
173+
return
174+
}
175+
if bucketInfo.Name == "" {
176+
http.Error(w, "bucket name is required", http.StatusBadRequest)
177+
return
178+
}
179+
180+
if err := createBucket(bucketInfo.Name); err != nil {
181+
http.Error(w, err.Error(), http.StatusConflict)
182+
return
183+
}
184+
185+
w.WriteHeader(http.StatusOK)
186+
json.NewEncoder(w).Encode(bucketInfo)
187+
}
188+
189+
// handleUploadObject uploads an object to a specified bucket.
190+
func handleUploadObject(w http.ResponseWriter, r *http.Request) {
191+
if r.Method != http.MethodPost {
192+
http.NotFound(w, r)
193+
return
194+
}
195+
196+
bucketName := r.PathValue("bucket")
197+
objectName := r.URL.Query().Get("name")
198+
if objectName == "" {
199+
objectName = r.PathValue("object")
200+
}
201+
202+
if bucketName == "" || objectName == "" {
203+
http.Error(w, "missing bucket or object name", http.StatusBadRequest)
204+
return
205+
}
206+
207+
data, err := io.ReadAll(r.Body)
208+
if err != nil {
209+
http.Error(w, "failed to read request body", http.StatusInternalServerError)
210+
return
211+
}
212+
defer r.Body.Close()
213+
214+
contentType := r.Header.Get("Content-Type")
215+
if contentType == "" {
216+
contentType = "application/octet-stream"
217+
}
218+
219+
response, err := uploadObject(bucketName, objectName, data, contentType)
220+
if err != nil {
221+
http.Error(w, err.Error(), http.StatusNotFound)
222+
return
223+
}
224+
225+
w.Header().Set("Content-Type", "application/json")
226+
json.NewEncoder(w).Encode(response)
227+
}
228+
229+
func createBucket(bucketName string) error {
230+
if _, exists := inMemoryStore[bucketName]; exists {
231+
return fmt.Errorf("bucket already exists")
232+
}
233+
inMemoryStore[bucketName] = make(map[string]ObjectData)
234+
log.Printf("created bucket: %s", bucketName)
235+
return nil
236+
}
237+
238+
func uploadObject(bucketName, objectName string, data []byte, contentType string) (*GCSObject, error) {
239+
if _, ok := inMemoryStore[bucketName]; !ok {
240+
return nil, fmt.Errorf("bucket not found")
241+
}
242+
243+
inMemoryStore[bucketName][objectName] = ObjectData{
244+
Data: data,
245+
ContentType: contentType,
246+
}
247+
log.Printf("created object '%s' in bucket '%s' with Content-Type '%s'",
248+
objectName, bucketName, contentType)
249+
250+
return &GCSObject{
251+
Kind: "storage#object",
252+
Name: objectName,
253+
Bucket: bucketName,
254+
Size: strconv.Itoa(len(data)),
255+
ContentType: contentType,
256+
}, nil
257+
}
258+
259+
// The in-memory store to hold ObjectData structs.
260+
var inMemoryStore = make(map[string]map[string]ObjectData)
261+
262+
// ObjectData stores the raw data and its content type.
263+
type ObjectData struct {
264+
Data []byte
265+
ContentType string
266+
}
267+
268+
// GCSListResponse mimics the structure of a real GCS object list response.
269+
type GCSListResponse struct {
270+
Kind string `json:"kind"`
271+
Items []GCSObject `json:"items"`
272+
}
273+
274+
// GCSObject mimics the structure of a GCS object resource with ContentType.
275+
type GCSObject struct {
276+
Kind string `json:"kind"`
277+
Name string `json:"name"`
278+
Bucket string `json:"bucket"`
279+
Size string `json:"size"`
280+
ContentType string `json:"contentType"`
281+
}
282+
283+
// Manifest represents the top-level structure of the YAML file
284+
type Manifest struct {
285+
Buckets map[string]Bucket `yaml:"buckets"`
286+
}
287+
288+
// Bucket represents each bucket and its files
289+
type Bucket struct {
290+
Files []File `yaml:"files"`
291+
}
292+
293+
// File represents each file entry inside a bucket
294+
type File struct {
295+
Path string `yaml:"path"`
296+
ContentType string `yaml:"content-type"`
297+
}
Binary file not shown.

0 commit comments

Comments
 (0)