mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 12:08:37 +01:00
Compare commits
1 Commits
fix/downlo
...
ci/trivy-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44492876b3 |
62
.github/workflows/trivy-scan.yml
vendored
Normal file
62
.github/workflows/trivy-scan.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: 🛡️ Trivy Security Scan
|
||||
|
||||
# Filesystem scan (Streamyfin ships no container image): finds vulnerable dependencies,
|
||||
# leaked secrets and misconfigurations, and reports them to GitHub code scanning.
|
||||
# Runs post-merge + weekly (not on PRs — dependency-review already gates PRs, and SARIF
|
||||
# upload needs a write token that fork PRs don't get).
|
||||
on:
|
||||
push:
|
||||
branches: [develop, master]
|
||||
paths:
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
- ".github/workflows/trivy-scan.yml"
|
||||
schedule:
|
||||
- cron: "50 7 * * 5" # Weekly, Friday 07:50 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: trivy-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
trivy:
|
||||
name: 🔎 Filesystem scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # upload SARIF to code scanning
|
||||
steps:
|
||||
- name: 📥 Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: 💾 Cache Trivy vulnerability DB
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/trivy
|
||||
key: trivy-db-${{ github.run_id }}
|
||||
restore-keys: trivy-db-
|
||||
|
||||
- name: 🔎 Run Trivy filesystem scan
|
||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||
with:
|
||||
scan-type: fs
|
||||
scan-ref: .
|
||||
scanners: vuln,secret,misconfig
|
||||
ignore-unfixed: true
|
||||
severity: CRITICAL,HIGH
|
||||
format: sarif
|
||||
output: trivy-results.sarif
|
||||
|
||||
- name: 📤 Upload results to code scanning
|
||||
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-fs
|
||||
@@ -166,7 +166,7 @@ export default function IndexLayout() {
|
||||
open={dropdownOpen}
|
||||
onOpenChange={setDropdownOpen}
|
||||
trigger={
|
||||
<View>
|
||||
<View className='pl-1.5'>
|
||||
<Ionicons
|
||||
name='ellipsis-horizontal-outline'
|
||||
size={24}
|
||||
|
||||
@@ -133,6 +133,7 @@ const HomeMobile = () => {
|
||||
onPress={() => {
|
||||
router.push("/(auth)/downloads");
|
||||
}}
|
||||
className='ml-1.5'
|
||||
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||
>
|
||||
<Feather
|
||||
|
||||
@@ -401,6 +401,10 @@ export const TVJellyseerrSearchResults: React.FC<
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const hasMovies = movieResults && movieResults.length > 0;
|
||||
const hasTv = tvResults && tvResults.length > 0;
|
||||
const hasPersons = personResults && personResults.length > 0;
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
@@ -427,26 +431,22 @@ export const TVJellyseerrSearchResults: React.FC<
|
||||
|
||||
return (
|
||||
<View>
|
||||
{/* No section requests `hasTVPreferredFocus`: the native search field
|
||||
keeps focus while typing, otherwise the first result would re-grab
|
||||
focus on every keystroke as results re-render. The user navigates
|
||||
down to the grid manually. */}
|
||||
<TVJellyseerrMovieSection
|
||||
title={t("search.request_movies")}
|
||||
items={movieResults}
|
||||
isFirstSection={false}
|
||||
isFirstSection={hasMovies}
|
||||
onItemPress={onMoviePress}
|
||||
/>
|
||||
<TVJellyseerrTvSection
|
||||
title={t("search.request_series")}
|
||||
items={tvResults}
|
||||
isFirstSection={false}
|
||||
isFirstSection={!hasMovies && hasTv}
|
||||
onItemPress={onTvPress}
|
||||
/>
|
||||
<TVJellyseerrPersonSection
|
||||
title={t("search.actors")}
|
||||
items={personResults}
|
||||
isFirstSection={false}
|
||||
isFirstSection={!hasMovies && !hasTv && hasPersons}
|
||||
onItemPress={onPersonPress}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -235,13 +235,10 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
||||
module). It renders the native search bar + grid keyboard and
|
||||
forwards typed text into the existing query pipeline via setSearch;
|
||||
our own results grid renders below. */}
|
||||
{/* No horizontal margin here: the native tvOS search bar centers itself
|
||||
and renders a trailing "Hold to Dictate in <Language>" hint. Extra
|
||||
margins squeeze the bar's width and clip that trailing hint, so let
|
||||
the native view span the full width and own its own insets. */}
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
marginHorizontal: HORIZONTAL_PADDING,
|
||||
height: SEARCH_AREA_HEIGHT,
|
||||
}}
|
||||
>
|
||||
@@ -283,17 +280,13 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
||||
{/* Library Search Results */}
|
||||
{isLibraryMode && !loading && (
|
||||
<View style={{ gap: SECTION_GAP }}>
|
||||
{sections.map((section) => (
|
||||
{sections.map((section, index) => (
|
||||
<TVSearchSection
|
||||
key={section.key}
|
||||
title={section.title}
|
||||
items={section.items!}
|
||||
orientation={section.orientation || "vertical"}
|
||||
// Never auto-focus a result. The native search field owns focus
|
||||
// while typing; `hasTVPreferredFocus` here would re-grab focus on
|
||||
// every keystroke as results re-render. User navigates down to the
|
||||
// grid manually.
|
||||
isFirstSection={false}
|
||||
isFirstSection={index === 0}
|
||||
onItemPress={onItemPress}
|
||||
onItemLongPress={onItemLongPress}
|
||||
imageUrlGetter={
|
||||
|
||||
@@ -297,12 +297,12 @@ export const TVSearchSection: React.FC<TVSearchSectionProps> = ({
|
||||
removeClippedSubviews={false}
|
||||
getItemLayout={getItemLayout}
|
||||
style={{ overflow: "visible" }}
|
||||
// Edge padding via contentContainerStyle, NOT contentInset+contentOffset.
|
||||
// contentOffset only applies on initial mount; since this FlatList is
|
||||
// reused across searches (stable key), a second search left the inset
|
||||
// without the offset and the grid snapped flush to the left edge.
|
||||
contentInset={{
|
||||
left: edgePadding,
|
||||
right: edgePadding,
|
||||
}}
|
||||
contentOffset={{ x: -edgePadding, y: 0 }}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: edgePadding,
|
||||
paddingVertical: SCALE_PADDING,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { File, Paths } from "expo-file-system";
|
||||
import { useCallback } from "react";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
|
||||
@@ -13,28 +12,36 @@ const useImageStorage = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* expo-file-system instead of fetch+Blob+FileReader: the latter silently
|
||||
* resolves to an empty payload under RN's New Architecture.
|
||||
*/
|
||||
const image2Base64 = useCallback(async (url?: string | null) => {
|
||||
if (!url) return null;
|
||||
|
||||
const tmpFile = new File(
|
||||
Paths.cache,
|
||||
`img-${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`,
|
||||
);
|
||||
let blob: Blob;
|
||||
try {
|
||||
const downloaded = await File.downloadFileAsync(url, tmpFile, {
|
||||
idempotent: true,
|
||||
});
|
||||
return await downloaded.base64();
|
||||
// Fetch the data from the URL
|
||||
const response = await fetch(url);
|
||||
blob = await response.blob();
|
||||
} catch (error) {
|
||||
console.warn("Error fetching image:", error);
|
||||
return null;
|
||||
} finally {
|
||||
if (tmpFile.exists) tmpFile.delete();
|
||||
}
|
||||
|
||||
// Create a FileReader instance
|
||||
const reader = new FileReader();
|
||||
|
||||
// Convert blob to base64
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
// Extract the base64 string (remove the data URL prefix)
|
||||
const base64 = reader.result.split(",")[1];
|
||||
resolve(base64);
|
||||
} else {
|
||||
reject(new Error("Failed to convert image to base64"));
|
||||
}
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveImage = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user