Skip to content

Commit 2c0b0fe

Browse files
authored
Merge pull request #12 from gatewayd-io/improvements
Fix training bugs, improve API, retrain model v3, and expand test coverage
2 parents a5376f5 + 58a7cf3 commit 2c0b0fe

16 files changed

Lines changed: 150 additions & 77 deletions

.github/workflows/test.yaml

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,22 @@ jobs:
1212
test:
1313
runs-on: ubuntu-22.04
1414
steps:
15-
- uses: actions/checkout@v3
15+
- uses: actions/checkout@v4
1616
- name: Set up Python
17-
uses: actions/setup-python@v2
17+
uses: actions/setup-python@v5
1818
with:
19-
python-version: 3.12
19+
python-version: "3.12"
2020
- name: Install dependencies
2121
run: |
2222
pip install poetry
2323
cd training && poetry install --with dev --no-root
24-
- name: Run formatter, linter and type checker
25-
run: |
26-
cd training && poetry run ruff check .
27-
# mypy --explicit-package-bases .
28-
# flake8 .
29-
# interrogate -vv --ignore-init-module --exclude sigma_api .
30-
- name: Run tests
24+
- name: Lint training code
25+
run: cd training && poetry run ruff check .
26+
- name: Lint API code
27+
run: cd training && poetry run ruff check ../api/
28+
- name: Run training tests
3129
run: cd training && poetry run pytest --cov=training --cov-report term --cov-report lcov:coverage.lcov -vv
32-
# - name: Submit coverage report to Coveralls
33-
# if: ${{ success() }}
34-
# uses: coverallsapp/github-action@1.1.3
35-
# with:
36-
# github-token: ${{ secrets.GITHUB_TOKEN }}
37-
# path-to-lcov: ./coverage.lcov
30+
- name: Install API dependencies
31+
run: cd training && poetry run pip install flask gunicorn
32+
- name: Run API tests
33+
run: cd training && poetry run pytest ../api/test_api.py -vv

Dockerfile

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
FROM tensorflow/tensorflow:latest
1+
FROM tensorflow/tensorflow:2.16.1
22

3-
ENV dataset=sqli_dataset2.csv
43
ENV KMP_AFFINITY=noverbose
54
ENV TF_CPP_MIN_LOG_LEVEL=3
6-
ENV DATASET_PATH=/app/${dataset}
5+
ENV VOCAB_PATH=/app/sql_tokenizer_vocab.json
6+
ENV MODEL_PATH=/app/sqli_model/3/
77
ENV WORKERS=4
88
ENV HOST=0.0.0.0
99
ENV PORT=8000
1010

1111
WORKDIR /app
12-
COPY api/api.py /app
13-
COPY api/pyproject.toml /app
14-
COPY api/poetry.lock /app
15-
COPY dataset/${dataset} /app
12+
COPY api/api.py /app/
13+
COPY api/pyproject.toml /app/
14+
COPY api/poetry.lock /app/
1615
COPY training/sql_tokenizer.py /app/
1716
COPY training/sql_tokenizer_vocab.json /app/
18-
COPY sqli_model/ /app/sqli_model/
17+
COPY sqli_model/3/ /app/sqli_model/3/
1918
RUN pip install --disable-pip-version-check poetry
2019
RUN poetry install --no-root
2120

api/api.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,45 @@
1-
from flask import Flask, jsonify, request
2-
import tensorflow as tf
3-
import pandas as pd
1+
import logging
42
import os
5-
from sql_tokenizer import SQLTokenizer # Import SQLTokenizer
3+
4+
import tensorflow as tf
5+
from flask import Flask, jsonify, request
6+
7+
from sql_tokenizer import SQLTokenizer
8+
9+
logging.basicConfig(
10+
level=logging.INFO,
11+
format="%(asctime)s [%(levelname)s] %(message)s",
12+
)
13+
logger = logging.getLogger(__name__)
614

715
app = Flask(__name__)
816

9-
# Constants and configurations
1017
MAX_WORDS = 10000
1118
MAX_LEN = 100
12-
DATASET_PATH = os.getenv("DATASET_PATH", "dataset/sqli_dataset1.csv")
19+
VOCAB_PATH = os.getenv("VOCAB_PATH", "sql_tokenizer_vocab.json")
1320
MODEL_PATH = os.getenv("MODEL_PATH", "/app/sqli_model/3/")
1421

15-
# Load dataset and initialize SQLTokenizer
16-
DATASET = pd.read_csv(DATASET_PATH)
1722
sql_tokenizer = SQLTokenizer(max_words=MAX_WORDS, max_len=MAX_LEN)
18-
sql_tokenizer.fit_on_texts(DATASET["Query"]) # Fit tokenizer on dataset
23+
sql_tokenizer.load_token_index(VOCAB_PATH)
24+
logger.info("Loaded tokenizer vocabulary from %s (%d tokens)", VOCAB_PATH, len(sql_tokenizer.token_index))
1925

20-
# Load the model using tf.saved_model.load and get the serving signature
2126
loaded_model = tf.saved_model.load(MODEL_PATH)
2227
model_predict = loaded_model.signatures["serving_default"]
28+
logger.info("Loaded model from %s", MODEL_PATH)
2329

2430

2531
def warm_up_model():
26-
"""Sends a dummy request to the model to 'warm it up'."""
32+
"""Sends a dummy request to the model to initialize it."""
2733
dummy_query = "SELECT * FROM users WHERE id = 1"
2834
query_seq = sql_tokenizer.texts_to_sequences([dummy_query])
2935
input_tensor = tf.convert_to_tensor(query_seq, dtype=tf.float32)
30-
_ = model_predict(input_tensor) # Make a dummy prediction to initialize the model
31-
print("Model warmed up and ready to serve requests.")
36+
_ = model_predict(input_tensor)
37+
logger.info("Model warmed up and ready to serve requests.")
38+
39+
40+
@app.route("/health", methods=["GET"])
41+
def health():
42+
return jsonify({"status": "ok"})
3243

3344

3445
@app.route("/predict", methods=["POST"])
@@ -37,27 +48,20 @@ def predict():
3748
return jsonify({"error": "No query provided"}), 400
3849

3950
try:
40-
# Tokenize and pad the input query using SQLTokenizer
4151
query = request.json["query"]
4252
query_seq = sql_tokenizer.texts_to_sequences([query])
4353
input_tensor = tf.convert_to_tensor(query_seq, dtype=tf.float32)
4454

45-
# Use the loaded model's serving signature to make the prediction
4655
prediction = model_predict(input_tensor)
4756

48-
# Check for valid output and extract the result
4957
if "output_0" not in prediction or prediction["output_0"].get_shape() != [1, 1]:
5058
return jsonify({"error": "Invalid model output"}), 500
5159

52-
# Extract confidence and return the response
53-
return jsonify(
54-
{
55-
"confidence": float("%.4f" % prediction["output_0"].numpy()[0][0]),
56-
}
57-
)
58-
except Exception as e:
59-
# Log the error and return a proper error message
60-
return jsonify({"error": str(e)}), 500
60+
confidence = float("%.4f" % prediction["output_0"].numpy()[0][0])
61+
return jsonify({"confidence": confidence})
62+
except Exception:
63+
logger.exception("Prediction failed")
64+
return jsonify({"error": "Internal server error"}), 500
6165

6266

6367
if __name__ == "__main__":

api/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ authors = ["Mostafa Moradian <mostafa@gatewayd.io>"]
66
readme = "README.md"
77

88
[tool.poetry.dependencies]
9-
python = ">=3.10,<3.11"
9+
python = "^3.12"
1010
Flask = "^3.0.2"
1111
gunicorn = "^21.2.0"
12-
pandas = "^2.2.1"
12+
pandas = "^2.2.2"
1313
numpy = "^1.26.4"
14-
tensorflow = "^2.15.0"
14+
tensorflow = "^2.16.1"
1515

1616
[build-system]
1717
requires = ["poetry-core"]

api/test_api.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import importlib
2+
import os
3+
import sys
4+
5+
import pytest
6+
7+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "training"))
8+
9+
os.environ.setdefault("VOCAB_PATH", os.path.join(
10+
os.path.dirname(__file__), "..", "training", "sql_tokenizer_vocab.json"))
11+
os.environ.setdefault("MODEL_PATH", os.path.join(
12+
os.path.dirname(__file__), "..", "sqli_model", "3"))
13+
14+
spec = importlib.util.spec_from_file_location(
15+
"api_module", os.path.join(os.path.dirname(__file__), "api.py"))
16+
api_module = importlib.util.module_from_spec(spec)
17+
spec.loader.exec_module(api_module)
18+
app = api_module.app
19+
20+
21+
@pytest.fixture
22+
def client():
23+
app.config["TESTING"] = True
24+
with app.test_client() as client:
25+
yield client
26+
27+
28+
def test_health(client):
29+
resp = client.get("/health")
30+
assert resp.status_code == 200
31+
assert resp.get_json() == {"status": "ok"}
32+
33+
34+
def test_predict_missing_body(client):
35+
resp = client.post("/predict", content_type="application/json")
36+
assert resp.status_code == 400
37+
38+
39+
def test_predict_missing_query_key(client):
40+
resp = client.post("/predict", json={"foo": "bar"})
41+
assert resp.status_code == 400
42+
data = resp.get_json()
43+
assert "error" in data
44+
45+
46+
def test_predict_sqli(client):
47+
resp = client.post("/predict", json={"query": "SELECT * FROM users WHERE id=1 OR 1=1"})
48+
assert resp.status_code == 200
49+
data = resp.get_json()
50+
assert "confidence" in data
51+
assert isinstance(data["confidence"], float)
52+
53+
54+
def test_predict_legitimate(client):
55+
resp = client.post("/predict", json={"query": "SELECT name FROM products"})
56+
assert resp.status_code == 200
57+
data = resp.get_json()
58+
assert "confidence" in data
59+
assert isinstance(data["confidence"], float)
60+
61+
62+
def test_predict_empty_query(client):
63+
resp = client.post("/predict", json={"query": ""})
64+
assert resp.status_code == 200
65+
data = resp.get_json()
66+
assert "confidence" in data
67+
68+
69+
def test_predict_error_not_leaked(client):
70+
"""Ensure internal error details are not exposed to the client."""
71+
resp = client.post("/predict", json={"query": ""})
72+
if resp.status_code == 500:
73+
data = resp.get_json()
74+
assert data["error"] == "Internal server error"

sqli_model/3/fingerprint.pb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
���־���鿶������月�� �����Ϗ�(�������2
1+
�����������ݺ���Y���月�� ��������(���վ����2:'306335063828443668507412436166038701185

sqli_model/3/saved_model.pb

131 KB
Binary file not shown.
40 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

training/requirements.txt

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)