Skip to content

Commit ebcd9b9

Browse files
authored
Merge branch 'main' into main
2 parents 6c12001 + 55fbfa3 commit ebcd9b9

12 files changed

Lines changed: 1256 additions & 355 deletions
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: VirusTotal Scan
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 17 * * *"
7+
pull_request:
8+
types: [opened, synchronize]
9+
10+
jobs:
11+
changes:
12+
runs-on: ubuntu-latest
13+
outputs:
14+
plugins: ${{ steps.filter.outputs.plugins }}
15+
steps:
16+
- name: Check out repository code
17+
uses: actions/checkout@v6
18+
- uses: dorny/paths-filter@v4
19+
if: github.event_name == 'pull_request'
20+
id: filter
21+
with:
22+
filters: |
23+
plugins:
24+
- 'plugins/**'
25+
26+
vt-plugin-scan:
27+
needs: changes
28+
if: needs.changes.outputs.plugins == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
29+
runs-on: ubuntu-latest
30+
steps:
31+
- name: Check out repository code
32+
uses: actions/checkout@v6
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v6
36+
with:
37+
python-version: "3.11"
38+
39+
- name: Install dependencies
40+
run: pip install requests
41+
42+
# Avoid repeat downloads which bump the plugins' download count
43+
- name: Restore downloaded plugin cache
44+
uses: actions/cache/restore@v5
45+
# No need to restore cache, PR's plugin is downloaded and scanned each time.
46+
# Plugin dev may repeatedly only update one version
47+
if: github.event_name != 'pull_request'
48+
with:
49+
path: |
50+
scan-files/
51+
plugin-download-cache.json
52+
key: plugin-zips-restore-not-used
53+
restore-keys: plugin-zips-
54+
55+
- name: Download plugin submitted in this PR
56+
if: github.event_name == 'pull_request'
57+
run: python ci/src/download_plugins.py --mode new --output-dir scan-files
58+
env:
59+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
61+
- name: Download all plugins with cache metadata
62+
if: github.event_name != 'pull_request'
63+
run: python ci/src/download_plugins.py --output-dir scan-files --cache-meta plugin-download-cache.json
64+
env:
65+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66+
67+
- name: Restore VirusTotal scan cache from previous runs
68+
uses: actions/cache/restore@v5
69+
# Don't use cache, upload/retrieve report from VirustTotal each time for new submissions
70+
if: github.event_name != 'pull_request'
71+
with:
72+
path: vt_cache.json
73+
key: vt-cache-restore-not-used
74+
restore-keys: vt-cache-
75+
76+
- name: Run VirusTotal scan
77+
uses: jjw24/virustotal-scanner-action@v1.0.1
78+
with:
79+
api-key: ${{ secrets.VT_API_KEY }}
80+
scan-paths: |
81+
./scan-files/
82+
no-cache: false
83+
cache-path: vt_cache.json
84+
whitelist-path: vt_whitelist.json
85+
report-path: vt_report.json
86+
request-interval-sec: 15
87+
analysis-poll-timeout-sec: 600
88+
download-timeout-sec: 120
89+
max-report-age-days: 30
90+
91+
- name: Save VirusTotal scan cache for future runs
92+
uses: actions/cache/save@v5
93+
# Cache not used for PR plugin submissions
94+
if: always() && github.event_name != 'pull_request'
95+
with:
96+
path: vt_cache.json
97+
key: vt-cache-${{ hashFiles('vt_cache.json') }}
98+
99+
- name: Save plugin ZIPs and cache metadata for future runs
100+
uses: actions/cache/save@v5
101+
# Upload even if scan fails
102+
if: always() && github.event_name != 'pull_request'
103+
with:
104+
path: |
105+
scan-files/
106+
plugin-download-cache.json
107+
key: plugin-zips-${{ hashFiles('plugin-download-cache.json') }}
108+
109+
- name: Upload VirusTotal scan report as viewable artifact
110+
uses: actions/upload-artifact@v7
111+
if: always()
112+
with:
113+
path: vt_report.json
114+
archive: false
115+
116+
- name: Upload VirusTotal scan cache as viewable artifact
117+
uses: actions/upload-artifact@v7
118+
# Cache not used for PR plugin submissions
119+
if: always() && github.event_name != 'pull_request'
120+
with:
121+
path: vt_cache.json
122+
archive: false
123+
124+
- name: Upload plugin download cache metadata as viewable artifact
125+
uses: actions/upload-artifact@v7
126+
# Upload even if scan fails
127+
if: always() && github.event_name != 'pull_request'
128+
with:
129+
path: plugin-download-cache.json
130+
archive: false
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: src unit tests
2+
3+
on:
4+
push:
5+
workflow_dispatch:
6+
7+
jobs:
8+
changes:
9+
runs-on: ubuntu-latest
10+
outputs:
11+
src_files: ${{ steps.filter.outputs.src_files }}
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: dorny/paths-filter@v4
15+
id: filter
16+
with:
17+
filters: |
18+
src_files:
19+
- 'ci/src/download_plugins.py'
20+
21+
unit-tests:
22+
needs: changes
23+
if: needs.changes.outputs.src_files == 'true'
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.11"
30+
- name: Install dependencies
31+
run: |
32+
pip install pytest requests
33+
- name: Run tests
34+
run: |
35+
python -m pytest tests/ -v

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Looking for a list of currently available plugins in Flow? Visit [here](https://
3131
"Language": "Programming language, e.g. python",
3232
"Website": "Plugin website, e.g. https://github.com/Flow-Launcher/Flow.Launcher.Plugin.HelloWorldPython",
3333
"UrlDownload": "URL to download, e.g. https://github.com/Flow-Launcher/Flow.Launcher.Plugin.HelloWorldPython/releases/download/v1.0.0/Flow.Launcher.Plugin.HelloWorldPython.zip",
34-
"UrlSourceCode": "URL to source code, e.g. https://github.com/Flow-Launcher/Flow.Launcher.Plugin.HelloWorldPython/tree/main",
34+
"UrlSourceCode": "URL to source code, e.g. https://github.com/Flow-Launcher/Flow.Launcher.Plugin.HelloWorldPython/tree/main",
3535
"IcoPath": "Plugin icon image's CDN URL, e.g. https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.Plugin.HelloWorldPython@main/Images/app.png"
3636
}
3737
```
@@ -58,6 +58,10 @@ Every three hours the *CI* in this repository will check for new updates from pl
5858

5959
So you do not need to manually submit a pull request after you make a new release.
6060

61+
## Security
62+
63+
All plugins submitted to the store are automatically scanned and re-scanned regularly with VirusTotal, a threat intelligence platform that aggregates detection results from over 70 antivirus engines. This helps identify malware, trojans, and other security risks before and after plugins are made available to users. This scanning provides an additional layer of protection for everyone using the Flow plugin store.
64+
6165
## Plugin Store policy
6266

6367
Plugins that facilitate or contain any of the following will not be allowed:

ci/src/_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*-coding: utf-8 -*-
2+
import hashlib
23
import json
34
import os
45
import re
@@ -170,6 +171,22 @@ def get_file_plugins_json_info(required_key: str = "") -> list[dict[str, str]]:
170171
return [{required_key: plugin[required_key]} for plugin in data]
171172

172173

174+
def sha256_file(path: Path) -> str:
175+
"""Compute the SHA-256 hex digest of a file.
176+
177+
Args:
178+
path: Path to the file.
179+
180+
Returns:
181+
Lower-case hex digest string.
182+
"""
183+
h = hashlib.sha256()
184+
with open(path, "rb") as f:
185+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
186+
h.update(chunk)
187+
return h.hexdigest()
188+
189+
173190
def get_new_plugin_submission_ids() -> list[str]:
174191
plugins_json_ids = [item["ID"] for item in get_file_plugins_json_info("ID")]
175192
existing_plugin_file_ids = [info["ID"] for info in plugin_reader()]

0 commit comments

Comments
 (0)