Skip to content

Commit 8133a7c

Browse files
committed
Merge branch 'main' into main-public
2 parents 00ff3ad + 2962dbf commit 8133a7c

File tree

53 files changed

+7665
-376
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+7665
-376
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Style Check and Lint
2+
on:
3+
pull_request:
4+
types: [submitted]
5+
# run the workflow if changes pushed to main or release branches
6+
push:
7+
branches: '**'
8+
paths: '**'
9+
10+
jobs:
11+
stylecheck-lint:
12+
name: Check codestsyle
13+
runs-on: [ xai-tlt ]
14+
container:
15+
image: ${{ vars.GHA_IMAGE }}
16+
env:
17+
http_proxy: ${{ secrets.HTTP_PROXY }}
18+
https_proxy: ${{ secrets.HTTPS_PROXY }}
19+
no_proxy: ${{ secrets.NO_PROXY }}
20+
# credentials:
21+
# username: ${{ secrets.REGISTRY_USER }}
22+
# password: ${{ secrets.REGISTRY_TOKEN }}
23+
volumes:
24+
- /tf_dataset/dataset/transfer_learning:/tmp/data
25+
steps:
26+
- name: Check out repository code
27+
uses: actions/[email protected]
28+
- name: Run stylecheck
29+
run: make stylecheck

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# the repo. Unless a later match takes precedence,
66
# @global-owner1 and @global-owner2 will be requested for
77
# review when someone opens a pull request.
8-
* @ashahba @daniel-de-leon-user293 @tybrs
8+
* @ashahba @daniel-de-leon-user293 @tybrs @mitalipo
99

1010
# Order is important; the last matching pattern takes the most
1111
# precedence. When someone opens a pull request that only

Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
#
1818

1919
VENV_DIR = ".venv"
20+
VENV_LINT = ".venv/lint"
2021
ACTIVATE_TEST = "$(VENV_DIR)/bin/activate"
22+
ACTIVATE_LINT = "$(VENV_LINT)/bin/activate"
2123
ACTIVATE_DOCS = $(ACTIVATE_TEST)
2224
ACTIVATE_NOTEBOOK = $(ACTIVATE_TEST)
2325

@@ -36,6 +38,14 @@ venv-test: poetry-lock
3638
pure-eval==0.2.2 \
3739
stack-data==0.6.3
3840

41+
venv-lint:
42+
@echo "Creating a virtual environment for linting $(VENV_LINT)..."
43+
@test -d $(VENV_LINT) || python -m virtualenv $(VENV_LINT) || python3 -m virtualenv $(VENV_LINT)
44+
@echo "Installing lint dependencies..."
45+
@. $(ACTIVATE_LINT) && pip install --no-cache-dir --no-deps \
46+
flake8==7.0.0 \
47+
black==24.4.2
48+
3949
test-mcg: venv-test
4050
@echo "Testing the Model Card Gen API..."
4151
@. $(ACTIVATE_TEST) && pytest model_card_gen/tests
@@ -77,6 +87,14 @@ test-notebook: venv-test
7787
@. $(ACTIVATE_NOTEBOOK) && \
7888
bash run_notebooks.sh $(CURDIR)/notebooks/explainer/imagenet_with_cam/ExplainingImageClassification.ipynb
7989

90+
stylecheck: venv-lint
91+
@echo "Checking code style..."
92+
@. $(ACTIVATE_LINT) flake8 . --config=tox.ini && echo "Code style is compatible with PEP 8 guidelines" || echo "Code style check failed. Please fix the above code style errors."
93+
94+
fix-codestyle: venv-lint
95+
@echo "Fixing code style..."
96+
@. $(ACTIVATE_LINT) black . --check --config=pyproject.toml
97+
8098
dist: build-whl
8199
@echo "Create binary wheel..."
82100

explainer/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ datasets = '2.14.4'
2424
deepdiff = '6.7.1'
2525
intel-tensorflow = '2.14.0'
2626
pytest = '8.1.1'
27-
scikit-learn = '1.4.0'
27+
scikit-learn = '1.5.0'
2828
tensorflow-hub = '0.15.0'
2929
torch = {version = "2.2.0", source = "pytorch-cpu"}
3030
torchvision = {version = "0.17.0", source = "pytorch-cpu"}

explainer/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def custom_tf_CNN():
9393
Creates and trains a simple TF CNN on the mnist dataset.
9494
Returns the model, a subset of the test dataset and the class names.
9595
96-
Taken from https://shap-lrjball.readthedocs.io/en/latest/example_notebooks/deep_explainer/Front%20Page%20DeepExplainer%20MNIST%20Example.html
96+
Taken from https://shap-lrjball.readthedocs.io/en/latest/example_notebooks/deep_explainer/Front%20Page%20DeepExplainer%20MNIST%20Example.html # noqa
9797
"""
9898
import tensorflow as tf
9999
from tensorflow.keras.datasets import mnist

fuzz/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
## Fuzz Testing in Intel Explainable AI Tools
2+
Fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. The program is then monitored for exceptions such as crashes, failing built-in code assertions, or potential memory leaks. This README details the use of Google's Atheris, a coverage-guided Python fuzzing engine, to conduct fuzz testing in our project.
3+
Inside this fuzz folder holds all fuzz testing programs.
4+
5+
### Requirements
6+
* Python: Version 3.9 or newer
7+
* Atheris: Google's fuzzing engine for Python
8+
* Coverage: Code coverage measurement for Python
9+
10+
## Setup
11+
To prepare your environment for fuzz testing with Atheris, follow these steps:
12+
13+
# Install Dependencies
14+
```
15+
pip install -r requirements.txt
16+
```
17+
## Running Fuzz Tests
18+
Example 1 (runs with a starting corpus and stops when interrupted by user):
19+
```
20+
python3 -m coverage run fuzz_test.py -atheris_runs=0 ../model_card_gen/intel_ai_safety/model_card_gen/docs/examples/json/
21+
```
22+
23+
Example 2 (runs for 10000 iterations and adds to coverage report instead of overwriting):
24+
```
25+
python3 -m coverage run -a fuzz_dataset.py -atheris_runs=10000
26+
```
27+
# Interpreting Results
28+
When running fuzz tests with Atheris it is important to understand the output to idenfity potential issues effectively.
29+
30+
### Crashes and Exceptions
31+
Atheris reports when the fuzzed input causes the program to crash or raise unhandled exceptions. These input are crucial for identifying vulnerabilities.
32+
33+
~~~
34+
ERROR: atheris detected an error in fuzz_test.py.
35+
CRASH: Test input caused an unhandled IndexError exception.
36+
~~~
37+
38+
In this example, the fuzzer has discovered an input that causes an IndexError in fuzz_test.py. This indicates that the code may not properly handle cases where list or array access is out of bounds. The developer should examine the stack trace provided by Atheris, identify whether there is problematic code, and implement proper bounds checking or error handling. If throwing the exception is the correct and expected behavior, the crash can be silently handled in fuzz_test.py using a try/except block.
39+
40+
### Coverage Metrics
41+
Atheris provides information about code coverage, which helps in understanding which parts of your code were exercised by the fuzz tests. Low coverage might indicate that additional fuzzing targets or more diverse inputs are needed.
42+
43+
To generate the coverage report, run the following command inside the fuzz folder:
44+
45+
`python3 -m coverage report`
46+
47+
The output will be:
48+
49+
| Name | Stmts | Miss | Cover |
50+
|-------------------------------------------------------------|-------|------|-------|
51+
| fuzz_test.py | 25 | 6 | 76% |
52+
| intel_ai_safety/model_card_gen/__init__.py | 0 | 0 | 100% |
53+
| intel_ai_safety/model_card_gen/analyze/__init__.py | 4 | 0 | 100% |
54+
| intel_ai_safety/model_card_gen/analyze/analyzer.py | 26 | 15 | 42% |
55+
| ... | ... | ... | ... |
56+
| intel_ai_safety/model_card_gen/validation.py | 26 | 12 | 54% |
57+
|-------------------------------------------------------------|-------|------|-------|
58+
| TOTAL | 835 | 416 | 50% |
59+
60+
Remember that the test may not be designed to exercise all the instrumented code, only a certain
61+
part or parts of it. It can be more helpful to look at the individual file coverage than the total.
62+
63+
The coverage report can also be viewed interactively, to inspect files or functions executed, using html:
64+
65+
`cd ../fuzz/htmlcov && python3 -m http.server`
66+
67+
Open http://localhost:8000/index.html in a web browser.
68+
69+
### Leak Detection
70+
71+
If you're using a Python extension module that interfaces with C code, you might encounter memory leaks due to improper memory management in the C layer. Here's an example of how a memory leak might be reported:
72+
73+
```plaintext
74+
Leak detected: an object of type 'MyCExtension.Object' with a size of 1024 bytes was not freed.
75+
Call stack of the allocation:
76+
File "my_c_extension.py", line 58, in create_object
77+
obj = MyCExtension.Object()
78+
```
79+
80+
Developers should review the create_object function to ensure proper memory management.
81+
82+
### Reproducing Issues
83+
For every failure, detected, Atheris outputs a test case that can reproduce the issues. These test cases can help debug and fix the vulnerabilities in your code. When Atheris encounters an issue such as an unhandled exception, it can provide a serialized input that caused the problem. This allows you to reproduce the issue for debugging purposes. Here's an example of the output you might see:
84+
85+
```plaintext
86+
EXCEPTION: Test input caused a KeyError in your Python code.
87+
Reproducing input written to: exception-abcdef1234567890.pickle
88+
To reproduce, run: python3 -m atheris reproduce exception-abcdef1234567890.pickle
89+
```
90+
91+
In this example, the fuzzer has discovered an input that causes a KeyError in the Python code. The input has been saved to a file named exception-abcdef1234567890.pickle. To reproduce the issue, the developer can run the provided command, which will execute the fuzzer with the exact same input that caused the exception, allowing for consistent reproduction and easier debugging.

fuzz/fuzz_dataset.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import atheris
2+
import numpy
3+
import random
4+
import sys
5+
6+
MIN_DATA_LENGTH = 1 # Minimum length of dataset
7+
MAX_DATA_LENGTH = 1000 # Maximum length of dataset
8+
9+
default_path = "../plugins/model_card_gen/generators/tfma/"
10+
sys.path.append(default_path)
11+
12+
with atheris.instrument_imports(include=["intel_ai_safety.*"]):
13+
from intel_ai_safety.model_card_gen.datasets.torch_datasets import PytorchNumpyDataset
14+
15+
16+
def TestOneInput(data):
17+
"""The entry point for the fuzzer."""
18+
fdp = atheris.FuzzedDataProvider(data)
19+
20+
# Create input and target numpy arrays of random but equal length
21+
# Label values will be integers between [0, 10]
22+
dataset_length = random.randint(MIN_DATA_LENGTH, MAX_DATA_LENGTH)
23+
input_array = numpy.array(fdp.ConsumeRegularFloatList(dataset_length))
24+
target_array = numpy.array(fdp.ConsumeIntListInRange(dataset_length, 0, 10))
25+
26+
dataset = PytorchNumpyDataset(input_array=input_array, target_array=target_array)
27+
assert len(dataset.dataset) == dataset_length
28+
29+
30+
if __name__ == "__main__":
31+
atheris.Setup(sys.argv, TestOneInput)
32+
atheris.Fuzz()

fuzz/fuzz_deep_explainer.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/python3
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Usage:
17+
python3 -m coverage run fuzz_deep_explainer.py -atheris_runs=10
18+
coverage report -m --omit=../fuzz/config*
19+
"""
20+
21+
import atheris
22+
import numpy as np
23+
import sys
24+
import itertools
25+
26+
default_path = "../plugins"
27+
sys.path.append(default_path)
28+
29+
# This tells Atheris to instrument all functions in the library
30+
with atheris.instrument_imports(include=["intel_ai_safety.explainer.attributions.attributions"]):
31+
from intel_ai_safety.explainer.attributions.attributions import deep_explainer
32+
33+
import torch
34+
from torchvision import datasets, transforms
35+
from torch import nn, optim
36+
from torch.nn import functional as F
37+
torch.manual_seed(0)
38+
39+
batch_size = 128
40+
num_epochs = 1
41+
device = torch.device('cpu')
42+
43+
44+
# MockNet class to replace the actual Net class for faster testing
45+
class MockNet(nn.Module):
46+
def __init__(self):
47+
48+
super(MockNet, self).__init__()
49+
self.fc_layers = nn.Sequential(
50+
nn.Linear(784, 10), # Assuming input is a flattened MNIST image (28x28)
51+
nn.Softmax(dim=1)
52+
)
53+
54+
def forward(self, x):
55+
x = x.view(-1, 784) # Flatten the image
56+
x = self.fc_layers(x)
57+
return x
58+
59+
60+
train_loader = torch.utils.data.DataLoader(
61+
datasets.MNIST('mnist_data', train=True, download=True,
62+
transform=transforms.Compose([
63+
transforms.ToTensor()
64+
])),
65+
batch_size=batch_size, shuffle=True)
66+
67+
test_loader = torch.utils.data.DataLoader(
68+
datasets.MNIST('mnist_data', train=False, transform=transforms.Compose([
69+
transforms.ToTensor()])), batch_size=batch_size, shuffle=True)
70+
71+
72+
@atheris.instrument_func
73+
def test_deep_explainer(input_bytes):
74+
75+
fdp = atheris.FuzzedDataProvider(input_bytes)
76+
# Generate random data based on the fuzzed input
77+
num_background = fdp.ConsumeIntInRange(1, 5)
78+
num_targets = fdp.ConsumeIntInRange(1, 5)
79+
num_classes = fdp.ConsumeIntInRange(2, 10)
80+
# The model expects images of shape (batch_size, channels, height, width)
81+
# For MNIST, this is typically (batch_size, 1, 28, 28)
82+
# Generate random images with the same shape
83+
background_images = np.random.rand(num_background, 1, 28, 28).astype(np.float32)
84+
target_images = np.random.rand(num_targets, 1, 28, 28).astype(np.float32)
85+
# The labels should be a list of strings, one for each class
86+
labels = [f"Class {i}" for i in range(num_classes)]
87+
# Use the mocked model instead of the actual Net to speedup the test
88+
model = MockNet().to(device)
89+
# Evaluate the model with a smaller subset of the test data to speedup the test
90+
model.eval()
91+
test_loss = 0
92+
correct = 0
93+
y_true = torch.empty(0)
94+
y_pred = torch.empty((0, 10))
95+
X_test = torch.empty((0, 1, 28, 28))
96+
97+
with torch.no_grad():
98+
for data, target in itertools.islice(test_loader, 10): # Limit the number of batches
99+
data, target = data.to(device), target.to(device)
100+
output = model(data)
101+
X_test = torch.cat((X_test, data))
102+
y_true, y_pred = torch.cat((y_true, target)), torch.cat((y_pred, output))
103+
104+
test_loss += F.nll_loss(output.log(), target).item()
105+
pred = output.max(1, keepdim=True)[1]
106+
correct += pred.eq(target.view_as(pred)).sum().item()
107+
108+
test_loss /= len(test_loader.dataset)
109+
classes = np.array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])
110+
# Use the generated background and target images for the deep explainer
111+
deep_explainer(model, torch.tensor(background_images), torch.tensor(target_images), classes)
112+
113+
return
114+
115+
116+
atheris.Setup(sys.argv, test_deep_explainer)
117+
atheris.Fuzz()

fuzz/fuzz_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import atheris
2+
import json
3+
import jsonschema
4+
import sys
5+
6+
STR_BYTE_COUNT = 10000 # Desired byte count for fuzzed strings
7+
8+
default_path = "../model_card_gen"
9+
sys.path.append(default_path)
10+
11+
with atheris.instrument_imports(include=["intel_ai_safety.*"]):
12+
from intel_ai_safety.model_card_gen.model_card_gen import ModelCardGen
13+
14+
15+
def mutate_schema(fdp, json_data):
16+
"""Recurses through a json object leaving keys and structures intact and
17+
randomly generating new data values of the proper type."""
18+
if isinstance(json_data, str):
19+
return fdp.ConsumeUnicode(STR_BYTE_COUNT)
20+
elif isinstance(json_data, list):
21+
return [mutate_schema(fdp, json_data[i]) for i in range(len(json_data))]
22+
elif isinstance(json_data, dict):
23+
return {k: mutate_schema(fdp, v) for k, v in json_data.items()}
24+
else:
25+
return None
26+
27+
28+
def TestOneInput(data):
29+
"""The entry point for the fuzzer."""
30+
try:
31+
json_data = json.loads(data)
32+
except json.decoder.JSONDecodeError:
33+
print("Not valid json")
34+
return
35+
except UnicodeDecodeError:
36+
print("Not valid unicode")
37+
return
38+
39+
fdp = atheris.FuzzedDataProvider(data)
40+
model_card_data = mutate_schema(fdp, json_data)
41+
try:
42+
mcg = ModelCardGen(data_sets={"test": ""}, model_card=model_card_data)
43+
if mcg.model_card:
44+
mcg.build_model_card() # Includes scaffold_assets() and export_format()
45+
except (ValueError, jsonschema.ValidationError):
46+
print("Doesn't match MC schema")
47+
return
48+
49+
50+
if __name__ == "__main__":
51+
atheris.Setup(sys.argv, TestOneInput)
52+
atheris.Fuzz()

0 commit comments

Comments
 (0)