3 Commits

Author SHA1 Message Date
b1957f2c19 adding import/export for configuration
All checks were successful
Build and Release to Staging / Build Vision Start (push) Successful in 46s
Build and Release to Staging / Build Vision Start Image (push) Successful in 2m22s
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 12s
2026-03-21 12:44:23 -03:00
efc9e5c3dd updating virustotal to v3 endpoints
All checks were successful
Build and Release to Staging / Build Vision Start (push) Successful in 7s
Build and Release / build (push) Successful in 38s
Build and Release to Staging / Build Vision Start Image (push) Successful in 1m15s
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 3s
Build and Release / virus-total-check (push) Successful in 49s
Build and Release / release (push) Successful in 5s
Build and Release / Build Vision Start Image (push) Successful in 1m1s
Build and Release / Deploy Vision Start (production) (push) Successful in 3s
2026-03-21 00:59:49 -03:00
65c6946e7f adjusting intervals
Some checks failed
Build and Release to Staging / Build Vision Start (push) Successful in 7s
Build and Release / build (push) Successful in 8s
Build and Release to Staging / Build Vision Start Image (push) Successful in 1m2s
Build and Release / virus-total-check (push) Failing after 37s
Build and Release / release (push) Has been skipped
Build and Release / Build Vision Start Image (push) Has been skipped
Build and Release / Deploy Vision Start (production) (push) Has been skipped
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 3s
2026-03-21 00:48:25 -03:00
3 changed files with 151 additions and 35 deletions

View File

@@ -59,8 +59,9 @@ jobs:
virustotal_apikey: ${{ secrets.VIRUSTOTAL_APIKEY }} virustotal_apikey: ${{ secrets.VIRUSTOTAL_APIKEY }}
VIRUS_TOTAL_FILE: vision-start-${{ gitea.ref_name }}.zip VIRUS_TOTAL_FILE: vision-start-${{ gitea.ref_name }}.zip
run: | run: |
# Run the VirusTotal check script and capture output # Run the VirusTotal check script and capture output in real-time
bash scripts/check_virustotal.sh > vt_output.txt 2>&1 set -o pipefail
bash scripts/check_virustotal.sh 2>&1 | tee vt_output.txt
# Extract analysis URL and detection ratio from output # Extract analysis URL and detection ratio from output
ANALYSIS_URL=$(grep "Analysis URL:" vt_output.txt | cut -d' ' -f3- || echo "Not available") ANALYSIS_URL=$(grep "Analysis URL:" vt_output.txt | cut -d' ' -f3- || echo "Not available")
@@ -70,9 +71,6 @@ jobs:
echo "analysis-url=$ANALYSIS_URL" >> $GITEA_OUTPUT echo "analysis-url=$ANALYSIS_URL" >> $GITEA_OUTPUT
echo "detection-ratio=$DETECTION_RATIO" >> $GITEA_OUTPUT echo "detection-ratio=$DETECTION_RATIO" >> $GITEA_OUTPUT
# Display the full output
cat vt_output.txt
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build, virus-total-check] needs: [build, virus-total-check]

View File

@@ -7,6 +7,26 @@ import Dropdown from './Dropdown';
import { baseWallpapers } from './utils/baseWallpapers'; import { baseWallpapers } from './utils/baseWallpapers';
import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager'; import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager';
const REQUIRED_LOCAL_STORAGE_KEYS = ['config', 'categories', 'userWallpapers', 'wallpaperState'] as const;
type RequiredLocalStorageKey = typeof REQUIRED_LOCAL_STORAGE_KEYS[number];
const safeParse = (value: string | null): unknown => {
if (value === null) {
return null;
}
try {
return JSON.parse(value);
} catch {
return value;
}
};
const toStorageString = (value: unknown): string => {
return typeof value === 'string' ? value : JSON.stringify(value);
};
interface ConfigurationModalProps { interface ConfigurationModalProps {
onClose: () => void; onClose: () => void;
onSave: (config: any) => void; onSave: (config: any) => void;
@@ -51,6 +71,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false); const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const importInputRef = useRef<HTMLInputElement>(null);
const isSaving = useRef(false); const isSaving = useRef(false);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@@ -236,6 +257,86 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
} }
}; };
const handleExportConfig = () => {
const exportPayload = {
version: 1,
exportedAt: new Date().toISOString(),
requiredLocalStorageKeys: [...REQUIRED_LOCAL_STORAGE_KEYS],
localStorage: REQUIRED_LOCAL_STORAGE_KEYS.reduce((acc, key) => {
acc[key] = safeParse(localStorage.getItem(key));
return acc;
}, {} as Record<RequiredLocalStorageKey, unknown>),
};
const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `vision-start-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleImportClick = () => {
importInputRef.current?.click();
};
const handleImportConfig = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
try {
const fileContent = await file.text();
const parsed = JSON.parse(fileContent);
const localStorageData = parsed?.localStorage && typeof parsed.localStorage === 'object'
? parsed.localStorage
: parsed;
if (!localStorageData || typeof localStorageData !== 'object') {
throw new Error('Invalid import file format.');
}
let importedAny = false;
REQUIRED_LOCAL_STORAGE_KEYS.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(localStorageData, key)) {
const rawValue = (localStorageData as Record<string, unknown>)[key];
localStorage.setItem(key, toStorageString(rawValue));
importedAny = true;
}
});
if (!importedAny) {
throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`);
}
const importedConfig = (localStorageData as Record<string, unknown>).config;
const importedUserWallpapers = (localStorageData as Record<string, unknown>).userWallpapers;
if (importedConfig && typeof importedConfig === 'object') {
setConfig(importedConfig as typeof config);
onWallpaperChange({ currentWallpapers: (importedConfig as { currentWallpapers?: string[] }).currentWallpapers || [] });
onSave(importedConfig);
}
if (Array.isArray(importedUserWallpapers)) {
setUserWallpapers(importedUserWallpapers as Wallpaper[]);
}
alert('Configuration imported successfully. The page will reload to apply all data.');
window.location.reload();
} catch (error) {
alert('Could not import configuration. Please use a valid export JSON file.');
console.error(error);
} finally {
event.target.value = '';
}
};
const allWallpapers = [...baseWallpapers, ...userWallpapers]; const allWallpapers = [...baseWallpapers, ...userWallpapers];
return ( return (
@@ -641,13 +742,30 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
)} )}
</div> </div>
<div className="p-8 border-t border-white/10"> <div className="p-8 border-t border-white/10">
<div className="flex justify-end gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex gap-2">
<button onClick={handleExportConfig} className="bg-slate-700 hover:bg-slate-600 active:scale-95 text-white text-sm font-semibold py-1.5 px-3 rounded-lg transition-all duration-150 ease-ios">
Export
</button>
<button onClick={handleImportClick} className="bg-slate-700 hover:bg-slate-600 active:scale-95 text-white text-sm font-semibold py-1.5 px-3 rounded-lg transition-all duration-150 ease-ios">
Import
</button>
<input
ref={importInputRef}
type="file"
accept="application/json"
className="hidden"
onChange={handleImportConfig}
/>
</div>
<div className="flex justify-end gap-4">
<button onClick={() => { isSaving.current = true; onSave(config); }} className="bg-green-500 hover:bg-green-400 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios"> <button onClick={() => { isSaving.current = true; onSave(config); }} className="bg-green-500 hover:bg-green-400 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios">
Save & Close Save & Close
</button> </button>
<button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios"> <button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios">
Cancel Cancel
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,7 +9,7 @@ set -e
# Configuration # Configuration
FILE_PATH="${VIRUS_TOTAL_FILE:-vision-start.zip}" FILE_PATH="${VIRUS_TOTAL_FILE:-vision-start.zip}"
API_KEY="${virustotal_apikey}" API_KEY="${virustotal_apikey}"
BASE_URL="https://www.virustotal.com/vtapi/v2" BASE_URL="https://www.virustotal.com/api/v3"
# Check if API key is set # Check if API key is set
if [ -z "$API_KEY" ]; then if [ -z "$API_KEY" ]; then
@@ -38,12 +38,12 @@ echo "Uploading $FILE_PATH to VirusTotal for analysis..."
# Upload file to VirusTotal # Upload file to VirusTotal
UPLOAD_RESPONSE=$(curl -s -X POST \ UPLOAD_RESPONSE=$(curl -s -X POST \
-F "apikey=$API_KEY" \ -H "x-apikey: $API_KEY" \
-F "file=@$FILE_PATH" \ -F "file=@$FILE_PATH" \
"$BASE_URL/file/scan") "$BASE_URL/files")
# Extract scan_id from response # Extract scan_id from response
SCAN_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.scan_id') SCAN_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id')
if [ "$SCAN_ID" == "null" ] || [ -z "$SCAN_ID" ]; then if [ "$SCAN_ID" == "null" ] || [ -z "$SCAN_ID" ]; then
echo "Error: Failed to upload file or get scan ID" echo "Error: Failed to upload file or get scan ID"
@@ -55,7 +55,7 @@ echo "File uploaded successfully. Scan ID: $SCAN_ID"
echo "Waiting for analysis to complete..." echo "Waiting for analysis to complete..."
# Wait for analysis to complete and get results # Wait for analysis to complete and get results
MAX_ATTEMPTS=30 MAX_ATTEMPTS=60
ATTEMPT=0 ATTEMPT=0
SLEEP_INTERVAL=10 SLEEP_INTERVAL=10
@@ -63,40 +63,40 @@ while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
echo "Checking analysis status (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..." echo "Checking analysis status (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..."
# Get scan report # Get scan report
REPORT_RESPONSE=$(curl -s -X POST \ REPORT_RESPONSE=$(curl -s -X GET \
-d "apikey=$API_KEY" \ -H "x-apikey: $API_KEY" \
-d "resource=$SCAN_ID" \ "$BASE_URL/analyses/$SCAN_ID")
"$BASE_URL/file/report")
# Check if analysis is complete # Check if analysis is complete
RESPONSE_CODE=$(echo "$REPORT_RESPONSE" | jq -r '.response_code') RESPONSE_CODE=$(echo "$REPORT_RESPONSE" | jq -r '.data.attributes.status')
if [ "$RESPONSE_CODE" == "1" ]; then if [ "$RESPONSE_CODE" == "completed" ]; then
# Analysis complete # Analysis complete
echo "Analysis completed!" echo "Analysis completed!"
# Extract results # Extract results
POSITIVES=$(echo "$REPORT_RESPONSE" | jq -r '.positives') POSITIVES=$(echo "$REPORT_RESPONSE" | jq -r '.data.attributes.stats.malicious')
TOTAL=$(echo "$REPORT_RESPONSE" | jq -r '.total') SUSPICIOUS=$(echo "$REPORT_RESPONSE" | jq -r '.data.attributes.stats.suspicious')
PERMALINK=$(echo "$REPORT_RESPONSE" | jq -r '.permalink') # The v3 analyses object has no 'total' field — compute it by summing all stat categories
TOTAL=$(echo "$REPORT_RESPONSE" | jq '[.data.attributes.stats | to_entries[].value] | add')
ANALYSIS_ID=$(echo "$REPORT_RESPONSE" | jq -r '.data.id')
PERMALINK="https://www.virustotal.com/gui/file-analysis/${ANALYSIS_ID}"
echo "Analysis URL: $PERMALINK" echo "Analysis URL: $PERMALINK"
echo "Detection ratio: $POSITIVES/$TOTAL" echo "Detection ratio: $POSITIVES/$TOTAL"
# Check if file is safe # Check if file is safe
if [ "$POSITIVES" -eq 0 ]; then if [ "$POSITIVES" -eq 0 ] && [ "$SUSPICIOUS" -eq 0 ]; then
echo "✅ File is clean (no threats detected)" echo "✅ File is clean (no threats detected)"
exit 0 exit 0
else else
echo "❌ File contains threats ($POSITIVES detections out of $TOTAL scanners)" echo "❌ File flagged: $POSITIVES malicious, $SUSPICIOUS suspicious (out of $TOTAL scanners)"
exit 1 exit 1
fi fi
elif [ "$RESPONSE_CODE" == "0" ]; then elif [ "$RESPONSE_CODE" == "queued" ]; then
# File not found or analysis not complete yet
echo "Analysis still in progress..."
elif [ "$RESPONSE_CODE" == "-2" ]; then
# Still queued for analysis
echo "File still queued for analysis..." echo "File still queued for analysis..."
elif [ "$RESPONSE_CODE" == "in-progress" ]; then
echo "Analysis still in progress..."
else else
echo "Unexpected response code: $RESPONSE_CODE" echo "Unexpected response code: $RESPONSE_CODE"
echo "Response: $REPORT_RESPONSE" echo "Response: $REPORT_RESPONSE"