Repository navigation
Expand file tree
/
Copy pathmalware.php
More file actions
89 lines (88 loc) · 7.03 KB
/
Copy pathmalware.php
File metadata and controls
89 lines (88 loc) · 7.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?php
declare(strict_types=1);
namespace OmaPlugCtl\Malware;
use function OmaPlugCtl\{home,state_root,executable,ensure_directory,locked,json_file,save_json,process_result,raw_run,now};
const MAX_BYTES=134217728;
const MAX_FILES=10000;
function portable_root(): string { return rtrim(getenv('XDG_DATA_HOME')?:home().'/.local/share','/').'/omaplugctl/scanner'; }
function binary(string $name): array {
$installed=executable($name);if($installed)return [$installed,getenv()];
$root=portable_root();$path=$root.'/usr/bin/'.$name;
if(is_file($path)&&is_executable($path))return [$path,[...getenv(),'LD_LIBRARY_PATH'=>$root.'/usr/lib','CVD_CERTS_DIR'=>$root.'/etc/clamav/certs']];
throw new \RuntimeException('ClamAV is unavailable. Install clamav before updating plugins, or change the scan setting in Config.');
}
function database(): string { return state_root().'/clamav'; }
function has_database(): bool { return (bool)(glob(database().'/daily.c[vl]d')?:[]); }
function status(): string {
try{binary('clamscan');}catch(\Throwable){return 'ClamAV is not installed';}
return has_database()?'ClamAV ready':'ClamAV signatures need downloading';
}
function refresh_signatures(): void {
ensure_directory(database());
locked(database().'/refresh.lock',function(){
$stamp=json_file(database().'/last-check.json');$checked=is_string($stamp['checkedAt']??null)?strtotime($stamp['checkedAt']):false;
if(has_database()&&$checked!==false&&time()-$checked<86400)return;
[$binary,$env]=binary('freshclam');$owner=getenv('USER')?:get_current_user();$config=database().'/freshclam.conf';
if(file_put_contents($config,'DatabaseDirectory '.database()."\nDatabaseOwner ".$owner."\nDatabaseMirror database.clamav.net\n")===false)throw new \RuntimeException('Could not prepare ClamAV signature refresh.');
try{$result=process_result([$binary,'--config-file',$config,'--stdout'],900,null,$env,4194304);file_put_contents(database().'/freshclam.log',$result['stdout'].$result['stderr']);}
catch(\Throwable $error){throw new \RuntimeException('ClamAV signature refresh failed. No plugin update applied.',0,$error);}
if($result['code']!==0||!has_database())throw new \RuntimeException('ClamAV signature refresh failed. No plugin update applied.');
save_json(['checkedAt'=>now()],database().'/last-check.json');
},true);
}
function tar_header(string $name,int $size): string {
// All payloads, including source symlinks, become regular files. Flattened
// names are bounded to fit the POSIX tar header without path traversal.
$header=str_pad($name,100,"\0")."0000644\0"."0000000\0"."0000000\0".sprintf('%011o',$size)."\0"."00000000000\0".str_repeat(' ',8).'0'.str_repeat("\0",100)."ustar\0".'00'.str_repeat("\0",32+32+8+8+155+12);
if(strlen($header)!==512)throw new \RuntimeException('Could not construct the scan snapshot.');
$sum=array_sum(unpack('C*',$header));return substr_replace($header,sprintf('%06o',$sum)."\0 ",148,8);
}
/** Stream the complete pinned tree to a tar file; keep the 128 MiB contract. */
function snapshot(string $root,string $commit,string $directory): array {
ensure_directory($directory);
$listing=raw_run(['git','-C',$root,'ls-tree','-rlz','--full-tree',$commit],30);
$entries=[];$total=0;
foreach(explode("\0",$listing)as$row) {
if($row==='')continue;$parts=explode("\t",$row,2);$head=preg_split('/\s+/',trim($parts[0]));
if(count($parts)!==2||count($head)!==4||$head[1]!=='blob'||!ctype_digit($head[3]))throw new \RuntimeException('Submodule contents cannot be fully scanned; update stopped.');
$size=(int)$head[3];$total+=$size;$entries[]=['oid'=>$head[2],'size'=>$size,'path'=>$parts[1]];
}
if(!$entries||count($entries)>MAX_FILES||$total>MAX_BYTES)throw new \RuntimeException('Plugin exceeds the complete-scan size limit; update stopped.');
$batch=$directory.'/objects';$archive=$directory.'/snapshot.tar';
$result=process_result(['git','-C',$root,'cat-file','--batch'],60,implode("\n",array_column($entries,'oid'))."\n",null,268435456,$batch);
if($result['code']!==0)throw new \RuntimeException('Could not read the complete Git snapshot.');
$input=fopen($batch,'rb');$output=fopen($archive,'wb');
if(!$input||!$output)throw new \RuntimeException('Could not stage the scan snapshot.');
try {
foreach($entries as$index=>$entry) {
if(fgets($input,200)!==$entry['oid'].' blob '.$entry['size']."\n")throw new \RuntimeException('Could not verify the Git snapshot for scanning.');
$name=sprintf('%06d-',$index).substr(preg_replace('/[^A-Za-z0-9_.-]/','_',basename($entry['path'])),0,93);
if(fwrite($output,tar_header($name,$entry['size']))!==512)throw new \RuntimeException('Could not stage scan archive.');
$remaining=$entry['size'];$first=true;
while($remaining>0) {
$body=fread($input,min(65536,$remaining));
if($body===false||$body==='')throw new \RuntimeException('Incomplete Git snapshot; update stopped.');
if($first&&str_starts_with($body,"version https://git-lfs.github.com/spec/v1\n"))throw new \RuntimeException('Git LFS payloads cannot be fully scanned; update stopped.');
$first=false;$remaining-=strlen($body);
if(fwrite($output,$body)!==strlen($body))throw new \RuntimeException('Could not stage scan archive.');
}
if(fread($input,1)!=="\n")throw new \RuntimeException('Incomplete Git snapshot; update stopped.');
$padding=(512-$entry['size']%512)%512;if($padding)fwrite($output,str_repeat("\0",$padding));
}
if(fread($input,1)!=='')throw new \RuntimeException('Unexpected trailing Git snapshot data.');
fwrite($output,str_repeat("\0",1024));
} finally {fclose($input);fclose($output);@unlink($batch);}
return [$archive,count($entries)];
}
function scan_commit(string $root,string $commit): array {
[$binary,$env]=binary('clamscan');refresh_signatures();
$directory=state_root().'/scans/'.bin2hex(random_bytes(10));ensure_directory($directory);
try {
[$payload,$count]=snapshot($root,$commit,$directory);
$result=process_result([$binary,'--database',database(),'--scan-archive=yes','--alert-exceeds-max=yes','--alert-encrypted=yes','--max-files=20000','--max-scansize=256M','--max-filesize=192M','--infected','--no-summary','--tempdir',$directory,'-'],300,null,$env,8388608,null,$payload);
if($result['code']===1)throw new \RuntimeException('ClamAV detected a threat or an unreadable archive; update stopped.');
if($result['code']!==0)throw new \RuntimeException('ClamAV could not complete the scan; update stopped.');
$version=process_result([$binary,'--database',database(),'--version'],10,null,$env);
return ['status'=>'no-threats-detected','commit'=>$commit,'files'=>$count,'scanner'=>\OmaPlugCtl\shorten(trim($version['stdout']),250),'snapshotSha256'=>hash_file('sha256',$payload),'checkedAt'=>now()];
} finally {foreach(glob($directory.'/*')?:[]as$file)if(is_file($file))@unlink($file);@rmdir($directory);}
}