Skip to content
Open
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
162 changes: 162 additions & 0 deletions .github/avogadro-security.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
rules:
# ── Code execution / injection ──
- id: avogadro-eval-exec
patterns:
- pattern-either:
- pattern: eval(...)
- pattern: exec(...)
- pattern: compile(..., ..., "exec")
message: >
eval()/exec()/compile() found. These allow arbitrary code execution
and are almost never needed in Avogadro plugins. Use structured data
processing (JSON, etc.) instead.
severity: ERROR
languages: [python]

- id: avogadro-dynamic-import
patterns:
- pattern-either:
- pattern: __import__(...)
- pattern: importlib.import_module(...)
message: >
Dynamic import detected. Plugins should use standard imports.
If loading optional dependencies, use try/except around normal imports.
severity: WARNING
languages: [python]

# ── Deserialization ──
- id: avogadro-unsafe-deserialization
patterns:
- pattern-either:
- pattern: pickle.load(...)
- pattern: pickle.loads(...)
- pattern: _pickle.load(...)
- pattern: marshal.load(...)
- pattern: marshal.loads(...)
- patterns:
- pattern: yaml.load(...)
- pattern-not: yaml.load(..., Loader=yaml.SafeLoader, ...)
- pattern-not: yaml.load(..., Loader=SafeLoader, ...)
- pattern: shelve.open(...)
message: >
Unsafe deserialization can execute arbitrary code. Use JSON or
other safe formats. If YAML is needed, use yaml.safe_load().
severity: ERROR
languages: [python]
Comment thread
ghutchis marked this conversation as resolved.

# ── Subprocess calls ──
- id: avogadro-subprocess-shell
patterns:
- pattern-either:
- pattern: subprocess.call(..., shell=True, ...)
- pattern: subprocess.run(..., shell=True, ...)
- pattern: subprocess.Popen(..., shell=True, ...)
- pattern: os.system(...)
- pattern: os.popen(...)
message: >
Shell command execution detected. Prefer subprocess with shell=False
and explicit argument lists. os.system() and os.popen() should never
be used.
severity: ERROR
languages: [python]

- id: avogadro-subprocess-review
patterns:
- pattern-either:
- pattern: subprocess.call(...)
- pattern: subprocess.run(...)
- pattern: subprocess.Popen(...)
- pattern: subprocess.check_output(...)
- pattern: subprocess.check_call(...)
message: >
Subprocess call detected. This is often legitimate for plugins that
wrap external tools (ORCA, Gaussian, xtb, etc.), but verify the
command is not constructed from untrusted input.
severity: WARNING
languages: [python]

# ── Network access ──
- id: avogadro-network-access
patterns:
- pattern-either:
- pattern: requests.get(...)
- pattern: requests.post(...)
- pattern: requests.put(...)
- pattern: requests.delete(...)
- pattern: urllib.request.urlopen(...)
- pattern: http.client.HTTPConnection(...)
- pattern: http.client.HTTPSConnection(...)
- pattern: socket.socket(...)
message: >
Network access detected. Verify the plugin genuinely needs network
access and that URLs are not constructed from untrusted input.
severity: WARNING
languages: [python]

# ── File system operations ──
- id: avogadro-dangerous-file-ops
patterns:
- pattern-either:
- pattern: shutil.rmtree(...)
- pattern: os.removedirs(...)
- pattern: pathlib.Path(...).unlink(...)
message: >
Destructive file operations detected. Verify paths are restricted
to the plugin's working directory.
severity: WARNING
languages: [python]

- id: avogadro-path-traversal
patterns:
- pattern-either:
- pattern: open($PATH + ..., ...)
- pattern: open(f"...{$VAR}...", ...)
- pattern: os.path.join(..., $USER_INPUT, ...)
message: >
Possible path traversal — file paths should be validated to prevent
accessing files outside the intended directory.
severity: WARNING
languages: [python]

# ── Credentials and secrets ──
- id: avogadro-hardcoded-secret
patterns:
- pattern-either:
- pattern: $VAR = "sk-..."
- pattern: $VAR = "api_key_..."
- pattern: |
password = "..."
- pattern: |
token = "..."
- pattern: |
secret = "..."
- pattern: |
api_key = "..."
message: >
Possible hardcoded credential. Use environment variables or
Avogadro's configuration system for secrets.
severity: ERROR
languages: [python]

# ── Native code / FFI ──
- id: avogadro-native-code
patterns:
- pattern-either:
- pattern: ctypes.cdll.LoadLibrary(...)
- pattern: ctypes.CDLL(...)
- pattern: cffi.FFI()
message: >
Native code loading via ctypes/cffi. This bypasses Python's safety
features and requires careful review.
severity: ERROR
languages: [python]

# ── Avogadro-specific best practices ──
- id: avogadro-stderr-for-progress
patterns:
- pattern: print(...) # in the context of a plugin script
message: >
Avogadro plugin scripts communicate via stdout (JSON). Use
sys.stderr.write() for progress messages, not print().
severity: INFO
languages: [python]
59 changes: 21 additions & 38 deletions .github/workflows/check-plugin-updates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,36 @@ on:
type: string

permissions:
contents: write
pull-requests: write
contents: read

jobs:
check-updates:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
updates: ${{ steps.detect.outputs.updates }}
count: ${{ steps.detect.outputs.count }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Detect upstream updates
id: detect
env:
PLUGIN_NAME: ${{ inputs.plugin_name }}
run: |
updates=$(python scripts/parse_plugins.py check-updates)
echo "Raw updates: $updates"

# If a specific plugin was requested, filter to just that one
if [ -n "${{ inputs.plugin_name }}" ]; then
updates=$(echo "$updates" | python3 -c "
import json, sys
data = json.load(sys.stdin)
filtered = [p for p in data if p['name'] == '${{ inputs.plugin_name }}']
print(json.dumps(filtered))
")
if [ -n "$PLUGIN_NAME" ]; then
updates=$(python3 scripts/parse_plugins.py check-updates --plugin-name "$PLUGIN_NAME")
else
updates=$(python3 scripts/parse_plugins.py check-updates)
fi
echo "Raw updates: $updates"

count=$(echo "$updates" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
echo "updates=$(echo "$updates" | jq -c .)" >> "$GITHUB_OUTPUT"
Expand All @@ -60,6 +59,9 @@ jobs:
needs: check-updates
if: needs.check-updates.outputs.count != '0'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
strategy:
# Process one plugin at a time to avoid branch conflicts
max-parallel: 1
Expand Down Expand Up @@ -125,31 +127,12 @@ jobs:
new_commit="${{ matrix.plugin.latest_commit }}"
latest_tag="${{ steps.commit-info.outputs.latest_tag }}"

# Replace the commit SHA
sed -i "s|$old_commit|$new_commit|" repositories.toml

# If we found a release tag, update or add release-tag
if [ -n "$latest_tag" ]; then
# Check if release-tag already exists for this plugin
if grep -A5 "^\[${plugin}\]" repositories.toml | grep -q "release-tag"; then
# Update existing release-tag (within the plugin's section)
python3 -c "
import re
with open('repositories.toml', 'r') as f:
content = f.read()
# Find the plugin section and update release-tag within it
pattern = r'(\[${plugin}\].*?release-tag\s*=\s*)\"[^\"]*\"'
content = re.sub(pattern, r'\1\"${latest_tag}\"', content, flags=re.DOTALL)
with open('repositories.toml', 'w') as f:
f.write(content)
"
else
# Add release-tag after the commit line
sed -i "/^\[${plugin}\]/,/^$\|^\[/ {
/git\.commit/a release-tag = \"${latest_tag}\"
}" repositories.toml
fi
fi
python3 scripts/update_plugin_entry.py \
repositories.toml \
"$plugin" \
"$old_commit" \
"$new_commit" \
--latest-tag "$latest_tag"

- name: Create Pull Request
if: steps.check-pr.outputs.skip == 'false'
Expand Down
Loading