#!/bin/sh
#
# System application architecture audit.
#
# Read-only POSIX shell tool for apps stored below:
#   /home/intel145/websites/apps/<app-name>
#
# Usage:
#   system.app.audit.sh APP_NAME [options]
#
# Examples:
#   sh system.app.audit.sh defapp --details --summary
#   sh system.app.audit.sh defapp \
#       --base-url https://apps.intellihometech.com \
#       --details \
#       --summary \
#       --report /tmp/defapp-system-audit.md
#
# This tool does not modify files, permissions, databases,
# Nginx configuration, services, or sessions.

set -eu

APPS_ROOT=${APPS_ROOT:-/home/intel145/websites/apps}
MAX_FILES=${MAX_FILES:-5000}

APP_NAME=
BASE_URL=
DETAILS=0
SUMMARY=0
INCLUDE_BACKUPS=0
REPORT_FILE=

PASS_COUNT=0
WARN_COUNT=0
FAIL_COUNT=0
ERROR_COUNT=0

usage() {
    cat <<'EOF'
Usage:
  system.app.audit.sh APP_NAME [options]

Options:
  --base-url ORIGIN   Optional origin only.
                      Example: https://apps.intellihometech.com
  --details           Print exact matching files and line numbers.
  --summary           Print final category totals.
  --backup            Include backup/editor residue checks.
  --report FILE       Write the complete report to FILE.
  -h, --help          Show this help.

Environment:
  APPS_ROOT           Apps root.
                      Default: /home/intel145/websites/apps
  MAX_FILES           Maximum file count before a scope warning.
                      Default: 5000

This audit is read-only. It never modifies files, permissions,
server configuration, services, databases, or sessions.
EOF
}

if [ "$#" -eq 0 ]; then
    usage
    exit 2
fi

APP_NAME=$1
shift

while [ "$#" -gt 0 ]; do
    case "$1" in
        --base-url)
            if [ "$#" -lt 2 ]; then
                echo "ERROR: --base-url requires an origin." >&2
                exit 2
            fi
            BASE_URL=$2
            shift 2
            ;;
        --details)
            DETAILS=1
            shift
            ;;
        --summary)
            SUMMARY=1
            shift
            ;;
        --backup)
            INCLUDE_BACKUPS=1
            shift
            ;;
        --report)
            if [ "$#" -lt 2 ]; then
                echo "ERROR: --report requires a file path." >&2
                exit 2
            fi
            REPORT_FILE=$2
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            echo "ERROR: unknown option: $1" >&2
            usage >&2
            exit 2
            ;;
    esac
done

case "$APP_NAME" in
    *[!A-Za-z0-9_-]*|'')
        echo "ERROR: APP_NAME may contain only letters, digits, hyphens, and underscores." >&2
        exit 2
        ;;
esac

if [ -n "$BASE_URL" ]; then
    case "$BASE_URL" in
        http://*|https://*)
            ;;
        *)
            echo "ERROR: --base-url must begin with http:// or https://." >&2
            exit 2
            ;;
    esac

    case "$BASE_URL" in
        */*)
            scheme_part=${BASE_URL%%://*}
            origin_rest=${BASE_URL#*://}

            case "$origin_rest" in
                */*)
                    echo "ERROR: --base-url must be an origin only; do not include an app path." >&2
                    echo "Example: https://apps.intellihometech.com" >&2
                    exit 2
                    ;;
            esac
            ;;
    esac
fi

if [ ! -d "$APPS_ROOT" ]; then
    echo "ERROR: APPS_ROOT does not exist: $APPS_ROOT" >&2
    exit 2
fi

APPS_ROOT_REAL=$(cd "$APPS_ROOT" && pwd -P)
APP_DIR=$APPS_ROOT/$APP_NAME

if [ ! -d "$APP_DIR" ]; then
    echo "ERROR: app directory does not exist: $APP_DIR" >&2
    exit 2
fi

APP_REAL=$(cd "$APP_DIR" && pwd -P)

case "$APP_REAL/" in
    "$APPS_ROOT_REAL/"*)
        ;;
    *)
        echo "ERROR: resolved app path escapes APPS_ROOT." >&2
        exit 2
        ;;
esac

TMP_BASE=${TMPDIR:-/tmp}/system_app_audit_$$

TMP_DETAILS=$TMP_BASE.details
TMP_REPORT=$TMP_BASE.report
TMP_WRAPPERS=$TMP_BASE.wrappers
TMP_LEGACY_WRAPPERS=$TMP_BASE.legacy_wrappers
TMP_PAGE_WRAPPERS=$TMP_BASE.page_wrappers
TMP_STYLE_TAGS=$TMP_BASE.style_tags
TMP_BACKUPS=$TMP_BASE.backups
TMP_SENSITIVE=$TMP_BASE.sensitive

cleanup() {
    rm -f \
        "$TMP_DETAILS" \
        "$TMP_REPORT" \
        "$TMP_WRAPPERS" \
        "$TMP_LEGACY_WRAPPERS" \
        "$TMP_PAGE_WRAPPERS" \
        "$TMP_STYLE_TAGS" \
        "$TMP_BACKUPS" \
        "$TMP_SENSITIVE"
}

trap cleanup EXIT HUP INT TERM

: > "$TMP_DETAILS"
: > "$TMP_REPORT"
: > "$TMP_WRAPPERS"
: > "$TMP_LEGACY_WRAPPERS"
: > "$TMP_PAGE_WRAPPERS"
: > "$TMP_STYLE_TAGS"
: > "$TMP_BACKUPS"
: > "$TMP_SENSITIVE"

record_line() {
    printf '%s\n' "$1" >> "$TMP_REPORT"
}

emit() {
    STATUS=$1
    FINDING_ID=$2
    MESSAGE=$3
    DETAIL_TEXT=${4:-}

    LINE=$(printf '%-7s %-11s %s' "$STATUS" "$FINDING_ID" "$MESSAGE")

    printf '%s\n' "$LINE"
    record_line "$LINE"

    case "$STATUS" in
        PASS)
            PASS_COUNT=$((PASS_COUNT + 1))
            ;;
        WARN)
            WARN_COUNT=$((WARN_COUNT + 1))
            ;;
        FAIL)
            FAIL_COUNT=$((FAIL_COUNT + 1))
            ;;
        ERROR)
            ERROR_COUNT=$((ERROR_COUNT + 1))
            ;;
    esac

    if [ -n "$DETAIL_TEXT" ]; then
        {
            printf '[%s] %s\n' "$FINDING_ID" "$MESSAGE"
            printf '%s\n' "$DETAIL_TEXT"
        } >> "$TMP_DETAILS"
    fi
}

count_lines() {
    if [ -s "$1" ]; then
        wc -l < "$1" | tr -d ' '
    else
        printf '0'
    fi
}

http_status() {
    URL=$1

    STATUS=$(
        curl \
            -sS \
            --max-time 8 \
            --connect-timeout 5 \
            -L \
            -o /dev/null \
            -w '%{http_code}' \
            "$URL" \
            2>/dev/null \
        || true
    )

    case "$STATUS" in
        [0-9][0-9][0-9])
            printf '%s' "$STATUS"
            ;;
        *)
            printf ''
            ;;
    esac
}

printf 'System application audit\n'
printf 'App: %s\n' "$APP_NAME"
printf 'Root: %s\n\n' "$APP_REAL"

record_line "System application audit"
record_line "App: $APP_NAME"
record_line "Root: $APP_REAL"
record_line ""

if [ -f "$APP_REAL/index.php" ] || [ -f "$APP_REAL/$APP_NAME.index.php" ]; then
    emit \
        PASS \
        STRUCT-002 \
        "A likely public PHP entry point exists."
else
    emit \
        WARN \
        STRUCT-002 \
        "No index.php or $APP_NAME.index.php found; register the public entry point in app policy."
fi

if [ -d "$APP_REAL/css" ]; then
    emit \
        PASS \
        STRUCT-003 \
        "CSS directory exists."
else
    emit \
        WARN \
        STRUCT-003 \
        "No css directory found."
fi

grep -RIn \
    --include='*.php' \
    -E 'class="[^"]*app-wrapper' \
    "$APP_REAL" \
    2>/dev/null \
    | while IFS= read -r MATCH; do
        MATCH_PATH=${MATCH%%:*}

        case "$MATCH_PATH" in
            */old/*|*/test/*|*/tests/*|*/fixture/*|*/fixtures/*|*/example/*|*/examples/*)
                printf '%s\n' "$MATCH" >> "$TMP_LEGACY_WRAPPERS"
                ;;
            *)
                printf '%s\n' "$MATCH" >> "$TMP_WRAPPERS"
                ;;
        esac
    done

ACTIVE_WRAPPER_COUNT=$(count_lines "$TMP_WRAPPERS")
LEGACY_WRAPPER_COUNT=$(count_lines "$TMP_LEGACY_WRAPPERS")

if [ "$ACTIVE_WRAPPER_COUNT" -le 1 ]; then
    emit \
        PASS \
        STRUCT-005 \
        "At most one active PHP file declares an app-wrapper root." \
        "Rule: Only the primary application layout may declare .app-wrapper."
else
    emit \
        FAIL \
        STRUCT-005 \
        "$ACTIVE_WRAPPER_COUNT active PHP files declare app-wrapper; review component ownership." \
        "Rule: Only the primary application layout may declare .app-wrapper.
Why: Include partials must not create a second AdminLTE grid/layout root.
Action: Keep .app-wrapper only in the primary application template; remove it from active include partials.
Matches:
$(cat "$TMP_WRAPPERS")"
fi

if [ "$LEGACY_WRAPPER_COUNT" -gt 0 ]; then
    emit \
        WARN \
        STRUCT-005 \
        "$LEGACY_WRAPPER_COUNT legacy/test/example files declare app-wrapper; review exclusions or move them outside the served root." \
        "Rule: Old/test/example copies should not remain reachable as active application source.
Action: Move unused material outside the web-served app root, remove it, or register an approved exclusion.
Matches:
$(cat "$TMP_LEGACY_WRAPPERS")"
fi

if [ -d "$APP_REAL/pages" ]; then
    grep -RIn \
        --include='*.php' \
        -E 'class="[^"]*app-wrapper' \
        "$APP_REAL/pages" \
        2>/dev/null \
        > "$TMP_PAGE_WRAPPERS" \
        || true

    PAGE_WRAPPER_COUNT=$(count_lines "$TMP_PAGE_WRAPPERS")

    if [ "$PAGE_WRAPPER_COUNT" -eq 0 ]; then
        emit \
            PASS \
            STRUCT-004 \
            "pages/ has no app-wrapper declaration."
    else
        emit \
            FAIL \
            STRUCT-004 \
            "pages/ contains app-wrapper markup; include partials must not create layout roots." \
            "Rule: Files under pages/ are include fragments and must not own .app-wrapper.
Why: Nested wrappers can duplicate headers, sidebars, grids, and layout spacing.
Action: Keep only the local fragment in pages/; move the single wrapper to the main app template.
Matches:
$(cat "$TMP_PAGE_WRAPPERS")"
    fi
else
    emit \
        WARN \
        STRUCT-004 \
        "No pages/ directory found."
fi

grep -RIn \
    --include='*.css' \
    -E '<[[:space:]]*/?[[:space:]]*style[[:space:]]*>' \
    "$APP_REAL" \
    2>/dev/null \
    > "$TMP_STYLE_TAGS" \
    || true

STYLE_TAG_COUNT=$(count_lines "$TMP_STYLE_TAGS")

if [ "$STYLE_TAG_COUNT" -eq 0 ]; then
    emit \
        PASS \
        HYG-001 \
        "No style tags found in external CSS files."
else
    emit \
        FAIL \
        HYG-001 \
        "External CSS contains HTML style tags." \
        "Rule: External .css files must contain raw CSS only.
Action: Remove <style> and </style> tags.
Matches:
$(cat "$TMP_STYLE_TAGS")"
fi

if [ "$INCLUDE_BACKUPS" -eq 1 ]; then
    find "$APP_REAL" \
        -type f \
        \( \
            -name '*.bak' \
            -o -name '*.backup' \
            -o -name '*~' \
            -o -name '*.swp' \
        \) \
        -print \
        2>/dev/null \
        > "$TMP_BACKUPS"

    BACKUP_COUNT=$(count_lines "$TMP_BACKUPS")

    if [ "$BACKUP_COUNT" -eq 0 ]; then
        emit \
            PASS \
            HYG-002 \
            "No common backup/editor-residue files found."
    else
        emit \
            WARN \
            HYG-002 \
            "$BACKUP_COUNT backup/editor-residue files found; review whether they are intentionally retained." \
            "Rule: Backups and editor residue should not remain in served application trees.
Action: Delete unneeded copies or move retained copies outside the served app root.
Matches:
$(cat "$TMP_BACKUPS")"
    fi
else
    emit \
        WARN \
        HYG-002 \
        "Backup/editor-residue check skipped; use --backup to enable it."
fi

find "$APP_REAL" \
    -type f \
    \( \
        -name '.env' \
        -o -name '*.ini' \
        -o -name '*.log' \
        -o -name '*.sql' \
        -o -name '*.sqlite' \
        -o -name '*.bak' \
        -o -name '*.backup' \
    \) \
    -print \
    2>/dev/null \
    > "$TMP_SENSITIVE"

SENSITIVE_COUNT=$(count_lines "$TMP_SENSITIVE")

if [ "$SENSITIVE_COUNT" -eq 0 ]; then
    emit \
        PASS \
        SEC-001 \
        "No common sensitive file types found in the app tree."
else
    emit \
        WARN \
        SEC-001 \
        "Sensitive file types exist; ensure they are outside web exposure or denied by server policy." \
        "Rule: Secrets, logs, database dumps, and backup files must not be publicly reachable.
Action: Move them outside the served app root where possible; otherwise request an Nginx deny rule.
Matches:
$(cat "$TMP_SENSITIVE")"
fi

FILE_COUNT=$(find "$APP_REAL" -type f -print 2>/dev/null | wc -l | tr -d ' ')

if [ "$FILE_COUNT" -gt "$MAX_FILES" ]; then
    emit \
        WARN \
        LIMIT-001 \
        "App has $FILE_COUNT files, exceeding MAX_FILES=$MAX_FILES; review scan policy."
else
    emit \
        PASS \
        LIMIT-001 \
        "App file count ($FILE_COUNT) is within configured audit limit."
fi

if [ -n "$BASE_URL" ]; then
    APP_BASE=${BASE_URL%/}/$APP_NAME

    ROOT_STATUS=$(http_status "$APP_BASE/")

    case "$ROOT_STATUS" in
        200|201|202|203|204|301|302|303|307|308)
            emit \
                PASS \
                ROUTE-001 \
                "Public app root returned HTTP $ROOT_STATUS."
            ;;
        '')
            emit \
                ERROR \
                ROUTE-001 \
                "Could not determine public app-root status." \
                "Action: Verify through browser DevTools Network and hosting/WAF logs."
            ;;
        *)
            emit \
                ERROR \
                ROUTE-001 \
                "Public app root returned HTTP $ROOT_STATUS; investigate browser/hosting/WAF behavior." \
                "Action: Do not treat an unreliable command-line response as proof that the application is broken."
            ;;
    esac

    if [ -f "$APP_REAL/css/app.css" ]; then
        CSS_STATUS=$(http_status "$APP_BASE/css/app.css")

        case "$CSS_STATUS" in
            200)
                emit \
                    PASS \
                    ROUTE-002 \
                    "Public CSS path returned HTTP 200."
                ;;
            '')
                emit \
                    ERROR \
                    ROUTE-002 \
                    "Could not determine public CSS status." \
                    "Action: Verify the CSS request in browser DevTools Network."
                ;;
            *)
                emit \
                    ERROR \
                    ROUTE-002 \
                    "Public CSS path returned HTTP $CSS_STATUS; verify browser and investigate hosting/WAF behavior." \
                    "Action: Do not block css/ in Nginx rules."
                ;;
        esac
    fi

    if [ -f "$APP_REAL/pages/breadcrumbs.php" ]; then
        PRIVATE_STATUS=$(http_status "$APP_BASE/pages/breadcrumbs.php")

        case "$PRIVATE_STATUS" in
            403|404|406)
                emit \
                    PASS \
                    ROUTE-003 \
                    "Internal breadcrumbs partial is not public (HTTP $PRIVATE_STATUS)."
                ;;
            200)
                emit \
                    FAIL \
                    ROUTE-003 \
                    "Internal breadcrumbs partial is publicly accessible (HTTP 200)." \
                    "Rule: Internal files under pages/ must not be public routes.
Action: Ask the Nginx/hosting configuration owner to deny /$APP_NAME/pages/.
Expected: HTTP 403, 404, or 406."
                ;;
            '')
                emit \
                    ERROR \
                    ROUTE-003 \
                    "Could not establish a reliable internal-partial route result." \
                    "Action: Verify through browser DevTools Network and hosting/WAF logs."
                ;;
            *)
                emit \
                    ERROR \
                    ROUTE-003 \
                    "Internal breadcrumbs route check returned HTTP $PRIVATE_STATUS." \
                    "Action: Investigate hosting/WAF behavior; do not classify 5xx as protected."
                ;;
        esac
    fi
else
    emit \
        WARN \
        ROUTE-000 \
        "No --base-url provided; HTTP route checks were skipped."
fi

if [ "$DETAILS" -eq 1 ] && [ -s "$TMP_DETAILS" ]; then
    printf '\nDetails\n'
    printf '%s\n' '-------'

    record_line ""
    record_line "Details"
    record_line "-------"

    cat "$TMP_DETAILS"

    cat "$TMP_DETAILS" >> "$TMP_REPORT"
fi

FINAL_SUMMARY=$(printf 'Summary: PASS=%s WARN=%s FAIL=%s ERROR=%s' \
    "$PASS_COUNT" \
    "$WARN_COUNT" \
    "$FAIL_COUNT" \
    "$ERROR_COUNT")

printf '\n%s\n' "$FINAL_SUMMARY"
record_line ""
record_line "$FINAL_SUMMARY"

if [ "$SUMMARY" -eq 1 ]; then
    printf '%s\n' "Counts: pass=$PASS_COUNT warning=$WARN_COUNT failure=$FAIL_COUNT error=$ERROR_COUNT"
    record_line "Counts: pass=$PASS_COUNT warning=$WARN_COUNT failure=$FAIL_COUNT error=$ERROR_COUNT"
fi

if [ -n "$REPORT_FILE" ]; then
    REPORT_DIR=$(dirname "$REPORT_FILE")

    if [ ! -d "$REPORT_DIR" ]; then
        mkdir -p "$REPORT_DIR"
    fi

    cp "$TMP_REPORT" "$REPORT_FILE"

    printf 'Report written: %s\n' "$REPORT_FILE"
fi

if [ "$FAIL_COUNT" -gt 0 ] || [ "$ERROR_COUNT" -gt 0 ]; then
    exit 1
fi

exit 0
