Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
81 changes: 56 additions & 25 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,63 @@ name: Publish Python 🐍 distributions 📦 to PyPI
on:
push:
tags:
- '*'
- "v*"

# Least privilege at the workflow level; the publish job widens `id-token` only.
permissions:
contents: read

jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI
build:
name: Build and verify distributions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Install build tooling
run: |
python -m pip install --upgrade pip build twine
- name: Verify the tag matches the packaged version
run: |
python - <<'PY'
import os, sys, tomllib
tag = os.environ["GITHUB_REF_NAME"].lstrip("v")
with open("pyproject.toml", "rb") as fh:
version = tomllib.load(fh)["project"]["version"]
if tag != version:
sys.exit(f"Tag {tag!r} does not match pyproject version {version!r}")
print(f"Building {version}")
PY
- name: Run the test-suite before publishing
run: |
python -m pip install -e ".[dev]"
python -m pytest -q
- name: Build sdist and wheel
run: |
python -m build
python -m twine check dist/*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dist
path: dist/

publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/deepdefend/
permissions:
# Trusted Publishing (OIDC). No long-lived API token is stored.
id-token: write
steps:
- uses: actions/checkout@master
- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: '3.12'
- name: Install pypa/setuptools
run: >-
python -m
pip install setuptools wheel
- name: Extract tag name
id: tag
run: echo ::set-output name=TAG_NAME::$(echo $GITHUB_REF | cut -d / -f 3)
- name: Update version in setup.py
run: >-
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" setup.py
- name: Build a binary wheel
run: >-
python setup.py sdist bdist_wheel
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: CI

on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Install ruff
run: python -m pip install "ruff>=0.6"
- name: Ruff
run: ruff check .

test:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install package
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Run tests
run: python -m pytest -q

build:
name: Build distribution
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Build sdist and wheel
run: |
python -m pip install --upgrade pip build twine
python -m build
python -m twine check dist/*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dist
path: dist/
12 changes: 9 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
__pycache__
*.pyc
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
venv/
venv/
.venv/
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 75b98813cfb7e663870a28c74366a1e99d7bfe79 # v0.6.9
hooks:
- id: ruff
args: [--fix]
147 changes: 147 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Changelog

All notable changes to DeepDefend are documented here. The format loosely
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0]

This release is a correctness and security pass over the whole package. Many
functions that previously returned *silently wrong* results (or crashed) now do
what their name and documentation claim. Because the numerical output of several
attacks and defenses changes, the minor version was bumped.

### Fixed — attacks

- **`jsma`** no longer raises on every call (it indexed a rank-4 Jacobian with
five indices and mutated an eager tensor in place). It is now a real
saliency-map attack that works on any input rank.
- **`spsa`** no longer raises on every call (it passed a tensor, not a
`tf.Variable`, to the optimizer and hard-coded rank-4 inputs). The gradient
estimate now uses the correct finite difference
`(L(x+δd) − L(x−δd)) / (2δ)`, the optimiser is Adam, and any number of
samples and dimensions is accepted.
- **`ead`** no longer returns the input unchanged with its default arguments.
It now performs a proper ISTA/proximal step on the elastic-net objective.
- **`cw`** is now the Carlini & Wagner L2 attack: the tanh-space optimisation
actually minimises distortion and the `c` / `kappa` parameters change the
result (previously they were inert). The untargeted target class is fixed at
the runner-up class instead of being recomputed every step.
- **`deepfool`** is now DeepFool: per-class decision boundaries, an optional
L∞ budget, and output that stays inside the valid data range (it used to
return values such as `-0.90` and `2.08` for `[0, 1]` inputs).
- **`fgsm`** output is clipped to the valid data range instead of overshooting
to `-0.01` / `1.007`.
- **All attacks** now accept sparse integer labels, one-hot labels and binary
targets; previously every attack rejected the sparse-label format used by most
Keras examples.
- **All attacks** now auto-detect `from_logits` (with an override), call the
model with `training=False`, and support an explicit `clip_min` / `clip_max`
for normalised inputs.
- **Targeted attacks** no longer freeze at step 0 (the early-stop mask had the
logic inverted for targeted mode).
- **`pgd`** gained `random_start` and no longer duplicates `bim`.
- **DeepFool / JSMA / SPSA** no longer project against the whole batch inside
their per-sample loops.
- **`word_swap` / `char_swap`** preserve whitespace, newlines and punctuation,
accept a `seed`, and `word_swap` no longer silently returns the input when
`swap_dict` is missing.

### Fixed — defenses

- **All preprocessing defenses** can now be saved and reloaded. Their layers
were previously defined inside the function body, so every defended model
failed at `model.save()` / `load_model()`.
- **`jpeg_compression`**, **`feature_squeezing`** and
**`total_variation_minimization`** no longer break the gradient: attacks
against a defended model used to raise `ValueError` (JPEG) or silently return
a zero perturbation (TV, quantisation). They now use a straight-through
gradient.
- **`median_smoothing`** is a real median filter again (it was calling
`avg_pool2d`).
- **`gaussian_blur`** builds an actual Gaussian kernel and honours `sigma`
(the parameter was previously ignored).
- **`pixel_deflection`** no longer raises on every call, works inside
`tf.function`, and is reproducible when a `seed` is given.
- **`adversarial_training`** generates adversarial examples in batches instead
of one sample at a time (~500× fewer model calls), honours the caller's
optimiser and loss instead of forcing Adam + categorical cross-entropy, and
accepts `epochs`, `batch_size`, `verbose` and a custom `attack`.
- **`defensive_distillation`** no longer applies softmax twice, scales the
distillation term by `T²`, evaluates the teacher in inference mode, and works
for both probability and logit outputs.
- **`adversarial_logit_pairing`** generates adversarial examples inside the
training step; the previous implementation paired two models on clean inputs.
- **`randomized_smoothing`** clips the noisy input, is reproducible when
seeded, and ships with `smooth_predict()` for Monte-Carlo prediction and a
certified-radius estimate.
- **`feature_denoising`** has an explicit `method` (`median` / `gaussian`) and a
new `FeatureDenoisingBlock` layer for feature-level denoising.
- **`input_transformation`** raises instead of silently returning an
undefended model when no transformation is given.
- **`word_masking`** preserves whitespace and accepts a `seed`.
- **All defenses** raise a clear `TypeError` for subclassed or multi-input
models instead of an obscure `AttributeError`, and reject invalid parameters
(`bit_depth` outside 1-8, `num_bins < 2`, `quality` outside 1-100, even kernel
sizes, negative noise levels, ...) instead of producing `NaN`.

### Added

- `deepdefend.__version__`, `deepdefend.__all__`, and lazy imports: every public
function is reachable as `deepdefend.<name>`, and `import deepdefend` no
longer imports TensorFlow.
- `deepdefend/smooth_predict` for Monte-Carlo randomized smoothing.
- 170 tests covering attacks, defenses, serialisation, gradients, label
formats, input validation and the public API.
- CI: lint + test matrix (3.10-3.12) + reproducible build on every push and PR.
- `SECURITY.md`, `CODE_OF_CONDUCT.md`, Dependabot, pre-commit and
`pyproject.toml`.

### Changed

- `setup.py` was replaced by `pyproject.toml`; the version is now static and
`pip install .` works from a checkout (it previously failed because the
version was a `{{VERSION_PLACEHOLDER}}`).
- Dependencies are bounded (`numpy<3`, `tensorflow>=2.13,<3`) and
`python_requires` is `>=3.9` (TensorFlow 2.x is not available on 3.6-3.8).
- `pgd` / `bim` / `mim` default `alpha` is now `epsilon / 4` (previously
`0.01`, which equals `epsilon` for the default settings).
- `jsma` defaults are `theta=0.0`, `gamma=0.2`.
- `cw` defaults are `epsilon=None` (unconstrained), `c=10.0`.
- `spsa` defaults are `num_steps=50`, `learning_rate=0.05`, `spsa_samples=32`
probe directions, `delta=epsilon/5`.
- `median_smoothing` is documented as an alias of `spatial_smoothing`.
- The publish workflow uses SHA-pinned actions, a tag/version consistency check,
a test gate, `python -m build`, `twine check`, PyPI Trusted Publishing (OIDC)
and a protected `pypi` environment.

### Removed

- **`gradient_masking`** (and the `GradientMaskingModel` wrapper) has been
deleted. It only masked *training* gradients and had no effect on the
gradients an attacker computes at inference time — attacks against a wrapped
model were bit-identical to attacks against the base model. Gradient masking
is a documented false defense (Athalye et al., 2018) that makes a model look
robust while leaving it vulnerable, so it does not belong in a robustness
library. Mask gradients in your own `train_step` if you want the training
regulariser, and never report it as a security measure.
- The `setup.py`-based `{{VERSION_PLACEHOLDER}}` release mechanism.

## [0.1.5]

- Added MIM and EAD attacks, word/character swap attacks, pixel deflection,
Gaussian blur, TV minimisation and median smoothing defenses, word masking,
and a support table.

## [0.1.4]

- Added the SPSA attack and the JPEG compression defense.

## [0.1.3]

- Added randomized smoothing, feature denoising, thermometer encoding,
adversarial logit pairing and spatial smoothing.

## [0.1.2]

- Three new attacks and three new defenses.
47 changes: 47 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Code of Conduct

## Our pledge

We as members, contributors, and maintainers pledge to make participation in the
DeepDefend community a harassment-free experience for everyone, regardless of
age, body size, visible or invisible disability, ethnicity, sex characteristics,
gender identity and expression, level of experience, education, socio-economic
status, nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our standards

Examples of behaviour that contributes to a positive environment:

- Demonstrating empathy and kindness toward other people.
- Being respectful of differing opinions, viewpoints, and experiences.
- Giving and gracefully accepting constructive feedback.
- Accepting responsibility and apologising to those affected by our mistakes.
- Focusing on what is best for the community and the project.

Examples of unacceptable behaviour:

- The use of sexualised language or imagery, and sexual attention of any kind.
- Trolling, insulting or derogatory comments, and personal or political attacks.
- Public or private harassment.
- Publishing others' private information without their explicit permission.
- Submitting attack code intended to harm third-party systems, or requesting
help to do so.
- Other conduct which could reasonably be considered inappropriate in a
professional setting.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behaviour may be
reported to the maintainers at **infinitode.ltd@gmail.com**. All complaints will
be reviewed and investigated promptly and fairly, and the privacy and security
of the reporter will be respected.

Maintainers may remove, edit or reject comments, commits, code, issues and other
contributions that are not aligned with this Code of Conduct, and will
communicate the reasons for moderation decisions when appropriate.

## Attribution

This Code of Conduct is adapted from the
[Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
Loading
Loading