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
4 changes: 4 additions & 0 deletions .github/workflows/frapast-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
pull_request:
branches: [ main, develop ]

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

jobs:
action-test:
name: Test Composite Action (action.yml)
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/scanner-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ permissions:
contents: read
security-events: write # required for SARIF upload to Code Scanning

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

jobs:
test:
name: Tests (Python ${{ matrix.python-version }})
Expand Down Expand Up @@ -114,7 +118,7 @@ jobs:
"

- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: frapast-self.sarif
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://img.shields.io/badge/tests-306%20passed-green.svg)](#test-suite-and-quality-assurance)
[![Rules](https://img.shields.io/badge/detectors-33%20active-blue.svg)](#rule-taxonomy)
[![Rules](https://img.shields.io/badge/detectors-28%20active-blue.svg)](#rule-taxonomy)
[![Benchmark Speed](https://img.shields.io/badge/speed-16%2C700%2B%20files%2Fsec-green.svg)](#industry-benchmarks-and-performance)
[![Privacy](https://img.shields.io/badge/privacy-100%25%20Local%20and%20Airgapped-blue.svg)](#data-privacy-and-air-gapped-execution)

Expand Down Expand Up @@ -90,7 +90,7 @@ frapAST includes live integration testing against local and staging Frappe bench

## Architectural Comparison

| Capability | Generic SAST (Bandit / SonarQube) | Cloud Static Scanners | frapAST |
| Capability | Generic SAST Tools | Cloud Static Scanners | frapAST |
|---|:---:|:---:|:---:|
| Frappe Framework Modeling | None (generic Python only) | Partial regex matching | Full (DocType JSONs, `hooks.py`, ORM, DocEvents, Reports) |
| Active Proof Verification | None (static alerts only) | None (static alerts only) | Two-Tier Active Proof (synthesizes live HTTP reproducers) |
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ runs:

- name: Upload SARIF to GitHub Code Scanning
if: ${{ inputs.upload-sarif == 'true' && always() }}
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ inputs.sarif-file }}
category: frapast
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
]
requires-python = ">=3.10"
Expand Down
4 changes: 4 additions & 0 deletions scanner/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@
last_patch.file_path.write_text(last_patch.modified_source, encoding="utf-8")
success_count += len(file_patches)
except Exception as exc:
rel = str(fpath.name) # keep output concise: just the filename

Check failure on line 303 in scanner/cli.py

View workflow job for this annotation

GitHub Actions / Tests (Python 3.12)

ruff (F841)

scanner/cli.py:303:4: F841 Local variable `rel` is assigned to but never used help: Remove assignment to unused variable `rel`

Check failure on line 303 in scanner/cli.py

View workflow job for this annotation

GitHub Actions / Tests (Python 3.11)

ruff (F841)

scanner/cli.py:303:4: F841 Local variable `rel` is assigned to but never used help: Remove assignment to unused variable `rel`

Check failure on line 303 in scanner/cli.py

View workflow job for this annotation

GitHub Actions / Tests (Python 3.13)

ruff (F841)

scanner/cli.py:303:4: F841 Local variable `rel` is assigned to but never used help: Remove assignment to unused variable `rel`

Check failure on line 303 in scanner/cli.py

View workflow job for this annotation

GitHub Actions / Tests (Python 3.10)

ruff (F841)

scanner/cli.py:303:4: F841 Local variable `rel` is assigned to but never used help: Remove assignment to unused variable `rel`
failure_details.append((str(fpath), f"{type(exc).__name__}: {exc}"))

return success_count, failure_details
Expand Down Expand Up @@ -503,6 +503,10 @@
frapast scan /path/to/erpnext
frapast scan /path/to/frappe-app --severity --format json
frapast scan --config scan_config.yaml

Exit Codes:
0 Success (clean scan, findings below failure threshold, or command succeeded)
1 Execution error, invalid configuration, or unhandled exception
""",
)
parser.add_argument("--version", action="version", version=_get_version_string())
Expand Down
7 changes: 5 additions & 2 deletions scanner/schema/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ def discover_doctype_json(app_roots: str | Path | list[str | Path] | tuple[str |
d for d in dirs
if d not in SKIP_DIRS and not (d.startswith(".") and d != ".")
]
parent_name = Path(dirpath).name
parent_name = os.path.basename(dirpath)
dir_path_obj: Path | None = None
for fname in sorted(filenames):
if fname.endswith(".json"):
stem = fname[:-5]
if parent_name == stem:
files.append(SourceFile(path=Path(dirpath) / fname, root=root))
if dir_path_obj is None:
dir_path_obj = Path(dirpath)
files.append(SourceFile(path=dir_path_obj / fname, root=root))
return files


Expand Down
2 changes: 1 addition & 1 deletion scanner/suppression.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def is_suppressed(
"""Return True if the candidate has an inline ``# frapast: ignore`` suppression.

Checks the same line as the finding and the immediately preceding line, which is
the standard convention from Bandit/ESLint/ruff.
the standard convention from popular linters and static analyzers.

Parameters
----------
Expand Down
4 changes: 2 additions & 2 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ <h1>Most scanners guess.<br><span class="fade">frapAST proves it.</span></h1>
<section class="bordered" id="why">
<div class="wrap">
<div class="section-head">
<h2>Why not just use Bandit or SonarQube?</h2>
<h2>Why not generic SAST scanners?</h2>
<p>Generic Python SAST tools analyze Frappe apps as generic scripts. Frappe applications are
metadata-driven: DocType JSON schemas, hooks.py event registrations, and whitelist decorators
cannot be understood by plain AST linters without producing heavy false-positive noise.</p>
Expand Down Expand Up @@ -373,7 +373,7 @@ <h2>Frequently Asked Questions</h2>
</div>
<div class="faq-item">
<button class="faq-question" type="button" aria-expanded="false">
<span>How is frapAST different from Bandit or SonarQube?</span>
<span>How is frapAST different from generic Python SAST tools?</span>
<span class="icon"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
Expand Down
2 changes: 1 addition & 1 deletion tests/test_all_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def setUp(self):

def test_rules_registry_integrity(self):
from scanner.rules.engine import ALL_RULES, RENAMED_TAXONOMY
self.assertGreaterEqual(len(ALL_RULES), 20)
self.assertEqual(len(ALL_RULES), 28)
for rule in ALL_RULES:
self.assertTrue(callable(rule))
# Taxonomy consistency assertion passes at import time
Expand Down
Loading