Skip to content

Commit 133987b

Browse files
dave-gray101mudler
andauthored
feat: HF /scan endpoint (#2566)
* start by checking /scan during the checksum update Signed-off-by: Dave Lee <[email protected]> * add back in golang side features: downloader/uri gets struct and scan function, gallery uses it, and secscan/models calls it. Signed-off-by: Dave Lee <[email protected]> * add a param to scan specific urls - useful for debugging Signed-off-by: Dave Lee <[email protected]> * helpful printouts Signed-off-by: Dave Lee <[email protected]> * fix offsets Signed-off-by: Dave Lee <[email protected]> * fix error and naming Signed-off-by: Dave Lee <[email protected]> * expose error Signed-off-by: Dave Lee <[email protected]> * fix json tags Signed-off-by: Dave Lee <[email protected]> * slight wording change Signed-off-by: Dave Lee <[email protected]> * go mod tidy - getting warnings Signed-off-by: Dave Lee <[email protected]> * split out python to make editing easier, add some simple code to delete contaminated entries from gallery Signed-off-by: Dave Lee <[email protected]> * o7 to my favorite part of our old name, go-skynet Signed-off-by: Dave Lee <[email protected]> * merge fix Signed-off-by: Dave Lee <[email protected]> * merge fix Signed-off-by: Dave Lee <[email protected]> * merge fix Signed-off-by: Dave Lee <[email protected]> * address review comments Signed-off-by: Dave Lee <[email protected]> * forgot secscan could accept multiple URL at once Signed-off-by: Dave Lee <[email protected]> * invert naming and actually use it Signed-off-by: Dave Lee <[email protected]> * missed cli/models.go Signed-off-by: Dave Lee <[email protected]> * Update .github/check_and_update.py Co-authored-by: Ettore Di Giacinto <[email protected]> Signed-off-by: Dave <[email protected]> --------- Signed-off-by: Dave Lee <[email protected]> Signed-off-by: Dave <[email protected]> Co-authored-by: Ettore Di Giacinto <[email protected]>
1 parent cbb93bd commit 133987b

File tree

15 files changed

+282
-125
lines changed

15 files changed

+282
-125
lines changed

.github/check_and_update.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import hashlib
2+
from huggingface_hub import hf_hub_download, get_paths_info
3+
import requests
4+
import sys
5+
import os
6+
7+
uri = sys.argv[0]
8+
file_name = uri.split('/')[-1]
9+
10+
# Function to parse the URI and determine download method
11+
def parse_uri(uri):
12+
if uri.startswith('huggingface://'):
13+
repo_id = uri.split('://')[1]
14+
return 'huggingface', repo_id.rsplit('/', 1)[0]
15+
elif 'huggingface.co' in uri:
16+
parts = uri.split('/resolve/')
17+
if len(parts) > 1:
18+
repo_path = parts[0].split('https://huggingface.co/')[-1]
19+
return 'huggingface', repo_path
20+
return 'direct', uri
21+
22+
def calculate_sha256(file_path):
23+
sha256_hash = hashlib.sha256()
24+
with open(file_path, 'rb') as f:
25+
for byte_block in iter(lambda: f.read(4096), b''):
26+
sha256_hash.update(byte_block)
27+
return sha256_hash.hexdigest()
28+
29+
def manual_safety_check_hf(repo_id):
30+
scanResponse = requests.get('https://huggingface.co/api/models/' + repo_id + "/scan")
31+
scan = scanResponse.json()
32+
if scan['hasUnsafeFile']:
33+
return scan
34+
return None
35+
36+
download_type, repo_id_or_url = parse_uri(uri)
37+
38+
new_checksum = None
39+
40+
# Decide download method based on URI type
41+
if download_type == 'huggingface':
42+
# Check if the repo is flagged as dangerous by HF
43+
hazard = manual_safety_check_hf(repo_id_or_url)
44+
if hazard != None:
45+
print(f'Error: HuggingFace has detected security problems for {repo_id_or_url}: {str(hazard)}', filename=file_name)
46+
sys.exit(5)
47+
# Use HF API to pull sha
48+
for file in get_paths_info(repo_id_or_url, [file_name], repo_type='model'):
49+
try:
50+
new_checksum = file.lfs.sha256
51+
break
52+
except Exception as e:
53+
print(f'Error from Hugging Face Hub: {str(e)}', file=sys.stderr)
54+
sys.exit(2)
55+
if new_checksum is None:
56+
try:
57+
file_path = hf_hub_download(repo_id=repo_id_or_url, filename=file_name)
58+
except Exception as e:
59+
print(f'Error from Hugging Face Hub: {str(e)}', file=sys.stderr)
60+
sys.exit(2)
61+
else:
62+
response = requests.get(repo_id_or_url)
63+
if response.status_code == 200:
64+
with open(file_name, 'wb') as f:
65+
f.write(response.content)
66+
file_path = file_name
67+
elif response.status_code == 404:
68+
print(f'File not found: {response.status_code}', file=sys.stderr)
69+
sys.exit(2)
70+
else:
71+
print(f'Error downloading file: {response.status_code}', file=sys.stderr)
72+
sys.exit(1)
73+
74+
if new_checksum is None:
75+
new_checksum = calculate_sha256(file_path)
76+
print(new_checksum)
77+
os.remove(file_path)
78+
else:
79+
print(new_checksum)

.github/checksum_checker.sh

Lines changed: 8 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,77 +14,14 @@ function check_and_update_checksum() {
1414
idx="$5"
1515

1616
# Download the file and calculate new checksum using Python
17-
new_checksum=$(python3 -c "
18-
import hashlib
19-
from huggingface_hub import hf_hub_download, get_paths_info
20-
import requests
21-
import sys
22-
import os
23-
24-
uri = '$uri'
25-
file_name = uri.split('/')[-1]
26-
27-
# Function to parse the URI and determine download method
28-
# Function to parse the URI and determine download method
29-
def parse_uri(uri):
30-
if uri.startswith('huggingface://'):
31-
repo_id = uri.split('://')[1]
32-
return 'huggingface', repo_id.rsplit('/', 1)[0]
33-
elif 'huggingface.co' in uri:
34-
parts = uri.split('/resolve/')
35-
if len(parts) > 1:
36-
repo_path = parts[0].split('https://huggingface.co/')[-1]
37-
return 'huggingface', repo_path
38-
return 'direct', uri
39-
40-
def calculate_sha256(file_path):
41-
sha256_hash = hashlib.sha256()
42-
with open(file_path, 'rb') as f:
43-
for byte_block in iter(lambda: f.read(4096), b''):
44-
sha256_hash.update(byte_block)
45-
return sha256_hash.hexdigest()
46-
47-
download_type, repo_id_or_url = parse_uri(uri)
48-
49-
new_checksum = None
50-
51-
# Decide download method based on URI type
52-
if download_type == 'huggingface':
53-
# Use HF API to pull sha
54-
for file in get_paths_info(repo_id_or_url, [file_name], repo_type='model'):
55-
try:
56-
new_checksum = file.lfs.sha256
57-
break
58-
except Exception as e:
59-
print(f'Error from Hugging Face Hub: {str(e)}', file=sys.stderr)
60-
sys.exit(2)
61-
if new_checksum is None:
62-
try:
63-
file_path = hf_hub_download(repo_id=repo_id_or_url, filename=file_name)
64-
except Exception as e:
65-
print(f'Error from Hugging Face Hub: {str(e)}', file=sys.stderr)
66-
sys.exit(2)
67-
else:
68-
response = requests.get(repo_id_or_url)
69-
if response.status_code == 200:
70-
with open(file_name, 'wb') as f:
71-
f.write(response.content)
72-
file_path = file_name
73-
elif response.status_code == 404:
74-
print(f'File not found: {response.status_code}', file=sys.stderr)
75-
sys.exit(2)
76-
else:
77-
print(f'Error downloading file: {response.status_code}', file=sys.stderr)
78-
sys.exit(1)
79-
80-
if new_checksum is None:
81-
new_checksum = calculate_sha256(file_path)
82-
print(new_checksum)
83-
os.remove(file_path)
84-
else:
85-
print(new_checksum)
17+
new_checksum=$(python3 ./check_and_update.py $uri)
18+
result=$?
8619

87-
")
20+
if [[ result -eq 5]]; then
21+
echo "Contaminated entry detected, deleting entry for $model_name..."
22+
yq eval -i "del([$idx])" "$input_yaml"
23+
return
24+
fi
8825

8926
if [[ "$new_checksum" == "" ]]; then
9027
echo "Error calculating checksum for $file_name. Skipping..."
@@ -94,7 +31,7 @@ else:
9431
echo "Checksum for $file_name: $new_checksum"
9532

9633
# Compare and update the YAML file if checksums do not match
97-
result=$?
34+
9835
if [[ $result -eq 2 ]]; then
9936
echo "File not found, deleting entry for $file_name..."
10037
# yq eval -i "del(.[$idx].files[] | select(.filename == \"$file_name\"))" "$input_yaml"

core/backend/llm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func ModelInference(ctx context.Context, s string, messages []schema.Message, im
5757
if _, err := os.Stat(modelFile); os.IsNotExist(err) {
5858
utils.ResetDownloadTimers()
5959
// if we failed to load the model, we try to download it
60-
err := gallery.InstallModelFromGallery(o.Galleries, modelFile, loader.ModelPath, gallery.GalleryModel{}, utils.DisplayDownloadFunction)
60+
err := gallery.InstallModelFromGallery(o.Galleries, modelFile, loader.ModelPath, gallery.GalleryModel{}, utils.DisplayDownloadFunction, o.EnforcePredownloadScans)
6161
if err != nil {
6262
return nil, err
6363
}

core/cli/models.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67

78
cliContext "github.com/mudler/LocalAI/core/cli/context"
@@ -24,7 +25,8 @@ type ModelsList struct {
2425
}
2526

2627
type ModelsInstall struct {
27-
ModelArgs []string `arg:"" optional:"" name:"models" help:"Model configuration URLs to load"`
28+
DisablePredownloadScan bool `env:"LOCALAI_DISABLE_PREDOWNLOAD_SCAN" help:"If true, disables the best-effort security scanner before downloading any files." group:"hardening" default:"false"`
29+
ModelArgs []string `arg:"" optional:"" name:"models" help:"Model configuration URLs to load"`
2830

2931
ModelsCMDFlags `embed:""`
3032
}
@@ -88,9 +90,15 @@ func (mi *ModelsInstall) Run(ctx *cliContext.Context) error {
8890
return err
8991
}
9092

93+
err = gallery.SafetyScanGalleryModel(model)
94+
if err != nil && !errors.Is(err, downloader.ErrNonHuggingFaceFile) {
95+
return err
96+
}
97+
9198
log.Info().Str("model", modelName).Str("license", model.License).Msg("installing model")
9299
}
93-
err = startup.InstallModels(galleries, "", mi.ModelsPath, progressCallback, modelName)
100+
101+
err = startup.InstallModels(galleries, "", mi.ModelsPath, !mi.DisablePredownloadScan, progressCallback, modelName)
94102
if err != nil {
95103
return err
96104
}

core/cli/run.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,27 @@ type RunCMD struct {
4242
Threads int `env:"LOCALAI_THREADS,THREADS" short:"t" help:"Number of threads used for parallel computation. Usage of the number of physical cores in the system is suggested" group:"performance"`
4343
ContextSize int `env:"LOCALAI_CONTEXT_SIZE,CONTEXT_SIZE" default:"512" help:"Default context size for models" group:"performance"`
4444

45-
Address string `env:"LOCALAI_ADDRESS,ADDRESS" default:":8080" help:"Bind address for the API server" group:"api"`
46-
CORS bool `env:"LOCALAI_CORS,CORS" help:"" group:"api"`
47-
CORSAllowOrigins string `env:"LOCALAI_CORS_ALLOW_ORIGINS,CORS_ALLOW_ORIGINS" group:"api"`
48-
LibraryPath string `env:"LOCALAI_LIBRARY_PATH,LIBRARY_PATH" help:"Path to the library directory (for e.g. external libraries used by backends)" default:"/usr/share/local-ai/libs" group:"backends"`
49-
CSRF bool `env:"LOCALAI_CSRF" help:"Enables fiber CSRF middleware" group:"api"`
50-
UploadLimit int `env:"LOCALAI_UPLOAD_LIMIT,UPLOAD_LIMIT" default:"15" help:"Default upload-limit in MB" group:"api"`
51-
APIKeys []string `env:"LOCALAI_API_KEY,API_KEY" help:"List of API Keys to enable API authentication. When this is set, all the requests must be authenticated with one of these API keys" group:"api"`
52-
DisableWebUI bool `env:"LOCALAI_DISABLE_WEBUI,DISABLE_WEBUI" default:"false" help:"Disable webui" group:"api"`
53-
OpaqueErrors bool `env:"LOCALAI_OPAQUE_ERRORS" default:"false" help:"If true, all error responses are replaced with blank 500 errors. This is intended only for hardening against information leaks and is normally not recommended." group:"api"`
54-
Peer2Peer bool `env:"LOCALAI_P2P,P2P" name:"p2p" default:"false" help:"Enable P2P mode" group:"p2p"`
55-
Peer2PeerToken string `env:"LOCALAI_P2P_TOKEN,P2P_TOKEN,TOKEN" name:"p2ptoken" help:"Token for P2P mode (optional)" group:"p2p"`
56-
ParallelRequests bool `env:"LOCALAI_PARALLEL_REQUESTS,PARALLEL_REQUESTS" help:"Enable backends to handle multiple requests in parallel if they support it (e.g.: llama.cpp or vllm)" group:"backends"`
57-
SingleActiveBackend bool `env:"LOCALAI_SINGLE_ACTIVE_BACKEND,SINGLE_ACTIVE_BACKEND" help:"Allow only one backend to be run at a time" group:"backends"`
58-
PreloadBackendOnly bool `env:"LOCALAI_PRELOAD_BACKEND_ONLY,PRELOAD_BACKEND_ONLY" default:"false" help:"Do not launch the API services, only the preloaded models / backends are started (useful for multi-node setups)" group:"backends"`
59-
ExternalGRPCBackends []string `env:"LOCALAI_EXTERNAL_GRPC_BACKENDS,EXTERNAL_GRPC_BACKENDS" help:"A list of external grpc backends" group:"backends"`
60-
EnableWatchdogIdle bool `env:"LOCALAI_WATCHDOG_IDLE,WATCHDOG_IDLE" default:"false" help:"Enable watchdog for stopping backends that are idle longer than the watchdog-idle-timeout" group:"backends"`
61-
WatchdogIdleTimeout string `env:"LOCALAI_WATCHDOG_IDLE_TIMEOUT,WATCHDOG_IDLE_TIMEOUT" default:"15m" help:"Threshold beyond which an idle backend should be stopped" group:"backends"`
62-
EnableWatchdogBusy bool `env:"LOCALAI_WATCHDOG_BUSY,WATCHDOG_BUSY" default:"false" help:"Enable watchdog for stopping backends that are busy longer than the watchdog-busy-timeout" group:"backends"`
63-
WatchdogBusyTimeout string `env:"LOCALAI_WATCHDOG_BUSY_TIMEOUT,WATCHDOG_BUSY_TIMEOUT" default:"5m" help:"Threshold beyond which a busy backend should be stopped" group:"backends"`
64-
Federated bool `env:"LOCALAI_FEDERATED,FEDERATED" help:"Enable federated instance" group:"federated"`
45+
Address string `env:"LOCALAI_ADDRESS,ADDRESS" default:":8080" help:"Bind address for the API server" group:"api"`
46+
CORS bool `env:"LOCALAI_CORS,CORS" help:"" group:"api"`
47+
CORSAllowOrigins string `env:"LOCALAI_CORS_ALLOW_ORIGINS,CORS_ALLOW_ORIGINS" group:"api"`
48+
LibraryPath string `env:"LOCALAI_LIBRARY_PATH,LIBRARY_PATH" help:"Path to the library directory (for e.g. external libraries used by backends)" default:"/usr/share/local-ai/libs" group:"backends"`
49+
CSRF bool `env:"LOCALAI_CSRF" help:"Enables fiber CSRF middleware" group:"api"`
50+
UploadLimit int `env:"LOCALAI_UPLOAD_LIMIT,UPLOAD_LIMIT" default:"15" help:"Default upload-limit in MB" group:"api"`
51+
APIKeys []string `env:"LOCALAI_API_KEY,API_KEY" help:"List of API Keys to enable API authentication. When this is set, all the requests must be authenticated with one of these API keys" group:"api"`
52+
DisableWebUI bool `env:"LOCALAI_DISABLE_WEBUI,DISABLE_WEBUI" default:"false" help:"Disable webui" group:"api"`
53+
DisablePredownloadScan bool `env:"LOCALAI_DISABLE_PREDOWNLOAD_SCAN" help:"If true, disables the best-effort security scanner before downloading any files." group:"hardening" default:"false"`
54+
OpaqueErrors bool `env:"LOCALAI_OPAQUE_ERRORS" default:"false" help:"If true, all error responses are replaced with blank 500 errors. This is intended only for hardening against information leaks and is normally not recommended." group:"hardening"`
55+
Peer2Peer bool `env:"LOCALAI_P2P,P2P" name:"p2p" default:"false" help:"Enable P2P mode" group:"p2p"`
56+
Peer2PeerToken string `env:"LOCALAI_P2P_TOKEN,P2P_TOKEN,TOKEN" name:"p2ptoken" help:"Token for P2P mode (optional)" group:"p2p"`
57+
ParallelRequests bool `env:"LOCALAI_PARALLEL_REQUESTS,PARALLEL_REQUESTS" help:"Enable backends to handle multiple requests in parallel if they support it (e.g.: llama.cpp or vllm)" group:"backends"`
58+
SingleActiveBackend bool `env:"LOCALAI_SINGLE_ACTIVE_BACKEND,SINGLE_ACTIVE_BACKEND" help:"Allow only one backend to be run at a time" group:"backends"`
59+
PreloadBackendOnly bool `env:"LOCALAI_PRELOAD_BACKEND_ONLY,PRELOAD_BACKEND_ONLY" default:"false" help:"Do not launch the API services, only the preloaded models / backends are started (useful for multi-node setups)" group:"backends"`
60+
ExternalGRPCBackends []string `env:"LOCALAI_EXTERNAL_GRPC_BACKENDS,EXTERNAL_GRPC_BACKENDS" help:"A list of external grpc backends" group:"backends"`
61+
EnableWatchdogIdle bool `env:"LOCALAI_WATCHDOG_IDLE,WATCHDOG_IDLE" default:"false" help:"Enable watchdog for stopping backends that are idle longer than the watchdog-idle-timeout" group:"backends"`
62+
WatchdogIdleTimeout string `env:"LOCALAI_WATCHDOG_IDLE_TIMEOUT,WATCHDOG_IDLE_TIMEOUT" default:"15m" help:"Threshold beyond which an idle backend should be stopped" group:"backends"`
63+
EnableWatchdogBusy bool `env:"LOCALAI_WATCHDOG_BUSY,WATCHDOG_BUSY" default:"false" help:"Enable watchdog for stopping backends that are busy longer than the watchdog-busy-timeout" group:"backends"`
64+
WatchdogBusyTimeout string `env:"LOCALAI_WATCHDOG_BUSY_TIMEOUT,WATCHDOG_BUSY_TIMEOUT" default:"5m" help:"Threshold beyond which a busy backend should be stopped" group:"backends"`
65+
Federated bool `env:"LOCALAI_FEDERATED,FEDERATED" help:"Enable federated instance" group:"federated"`
6566
}
6667

6768
func (r *RunCMD) Run(ctx *cliContext.Context) error {
@@ -92,6 +93,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
9293
config.WithApiKeys(r.APIKeys),
9394
config.WithModelsURL(append(r.Models, r.ModelArgs...)...),
9495
config.WithOpaqueErrors(r.OpaqueErrors),
96+
config.WithEnforcedPredownloadScans(!r.DisablePredownloadScan),
9597
}
9698

9799
token := ""

core/cli/util.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
package cli
22

33
import (
4+
"encoding/json"
5+
"errors"
46
"fmt"
57

68
"github.com/rs/zerolog/log"
79

810
cliContext "github.com/mudler/LocalAI/core/cli/context"
11+
"github.com/mudler/LocalAI/core/config"
12+
"github.com/mudler/LocalAI/core/gallery"
13+
"github.com/mudler/LocalAI/pkg/downloader"
914
gguf "github.com/thxcode/gguf-parser-go"
1015
)
1116

1217
type UtilCMD struct {
1318
GGUFInfo GGUFInfoCMD `cmd:"" name:"gguf-info" help:"Get information about a GGUF file"`
19+
HFScan HFScanCMD `cmd:"" name:"hf-scan" help:"Checks installed models for known security issues. WARNING: this is a best-effort feature and may not catch everything!"`
1420
}
1521

1622
type GGUFInfoCMD struct {
1723
Args []string `arg:"" optional:"" name:"args" help:"Arguments to pass to the utility command"`
1824
Header bool `optional:"" default:"false" name:"header" help:"Show header information"`
1925
}
2026

27+
type HFScanCMD struct {
28+
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
29+
Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"`
30+
ToScan []string `arg:""`
31+
}
32+
2133
func (u *GGUFInfoCMD) Run(ctx *cliContext.Context) error {
2234
if u.Args == nil || len(u.Args) == 0 {
2335
return fmt.Errorf("no GGUF file provided")
@@ -53,3 +65,37 @@ func (u *GGUFInfoCMD) Run(ctx *cliContext.Context) error {
5365

5466
return nil
5567
}
68+
69+
func (hfscmd *HFScanCMD) Run(ctx *cliContext.Context) error {
70+
log.Info().Msg("LocalAI Security Scanner - This is BEST EFFORT functionality! Currently limited to huggingface models!")
71+
if len(hfscmd.ToScan) == 0 {
72+
log.Info().Msg("Checking all installed models against galleries")
73+
var galleries []config.Gallery
74+
if err := json.Unmarshal([]byte(hfscmd.Galleries), &galleries); err != nil {
75+
log.Error().Err(err).Msg("unable to load galleries")
76+
}
77+
78+
err := gallery.SafetyScanGalleryModels(galleries, hfscmd.ModelsPath)
79+
if err == nil {
80+
log.Info().Msg("No security warnings were detected for your installed models. Please note that this is a BEST EFFORT tool, and all issues may not be detected.")
81+
} else {
82+
log.Error().Err(err).Msg("! WARNING ! A known-vulnerable model is installed!")
83+
}
84+
return err
85+
} else {
86+
var errs error = nil
87+
for _, uri := range hfscmd.ToScan {
88+
log.Info().Str("uri", uri).Msg("scanning specific uri")
89+
scanResults, err := downloader.HuggingFaceScan(uri)
90+
if err != nil && !errors.Is(err, downloader.ErrNonHuggingFaceFile) {
91+
log.Error().Err(err).Strs("clamAV", scanResults.ClamAVInfectedFiles).Strs("pickles", scanResults.DangerousPickles).Msg("! WARNING ! A known-vulnerable model is included in this repo!")
92+
errs = errors.Join(errs, err)
93+
}
94+
}
95+
if errs != nil {
96+
return errs
97+
}
98+
log.Info().Msg("No security warnings were detected for your installed models. Please note that this is a BEST EFFORT tool, and all issues may not be detected.")
99+
return nil
100+
}
101+
}

core/config/application_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type ApplicationConfig struct {
3131
PreloadModelsFromPath string
3232
CORSAllowOrigins string
3333
ApiKeys []string
34+
EnforcePredownloadScans bool
3435
OpaqueErrors bool
3536
P2PToken string
3637

@@ -301,6 +302,12 @@ func WithApiKeys(apiKeys []string) AppOption {
301302
}
302303
}
303304

305+
func WithEnforcedPredownloadScans(enforced bool) AppOption {
306+
return func(o *ApplicationConfig) {
307+
o.EnforcePredownloadScans = enforced
308+
}
309+
}
310+
304311
func WithOpaqueErrors(opaque bool) AppOption {
305312
return func(o *ApplicationConfig) {
306313
o.OpaqueErrors = opaque

0 commit comments

Comments
 (0)