Skip to content

Commit e5a3229

Browse files
Merge branch 'Flow-Launcher:main' into main
2 parents 800de92 + 327bf14 commit e5a3229

14 files changed

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