Skill: britrik/formatferry-markdown (version 1.1.3)

--- name: formatferry version: 1.1.3 description: Convert HTML, DOCX, PDF, XLSX, CSV to Markdown homepage: https://formatferry.vibingfun.com installation: npm install -g formatferry --- # formatferry Skill Convert HTML, DOCX, PDF, XLSX, CSV files to Markdown using the Format Ferry CLI. ## Installation Install the CLI globally: ```bash npm install -g formatferry ``` Or use npx (requires Node.js): ```bash npx formatferry --help ``` ## Authentication The user has a private API key. **Do NOT expose this key in any output or messages.** ```bash # Authenticate scripts/auth.sh --key <API_KEY> # Check status scripts/auth.sh --status ``` ## Scripts This skill includes secure wrapper scripts with **no `eval`** and **bash arrays** for safe argument handling: ### `scripts/convert-to-md.sh` (recommended) ```bash # Convert a local file scripts/convert-to-md.sh --input /path/to/file.pdf --output output.md --format github # Convert a URL scripts/convert-to-md.sh --url https://example.com/article --output article.md ``` ### `scripts/convert.sh` (legacy) ```bash scripts/convert.sh --file input.html --output result.md --format github scripts/convert.sh --url https://example.com --output article.md ``` **Supported formats:** github, commonmark, slack, discord, reddit, confluence, custom, rmarkdown ## Supported File Types - `.html` - Web pages - `.docx` - Word documents - `.pdf` - PDF files (up to 20MB) - `.xlsx` - Excel spreadsheets - `.csv` - CSV files ## Security Notes - **No eval:** All scripts use bash arrays for safe argument passing - **No OOM risk:** Output streams directly to file + stdout, never captured in shell variables - **Recursive path sanitization:** Loops repeatedly to strip `....//`, `..../`, `./`, and encoded bypasses (`%2e%2e`) - **Path validation:** Blocks writes to `/etc`, `/root`, `/sys`, `/proc` and sensitive files (`passwd`, `shadow`, `id_rsa`) - **Realpath verification:** Validates final paths are within allowed WORKDIR before execution - **URL validation:** Rejects malformed URLs (must start with http:// or https://) ## Provenance - **Homepage:** https://formatferry.vibingfun.com - **npm Package:** formatferry (official, no public repo) ## Notes - API key stored locally after authentication - Premium features may require a license key - Output format defaults to "github" if not specified

path: scripts/auth.sh
content: #!/usr/bin/env bash
#
# FormatFerry Auth Script
# Secure wrapper for authentication - no eval, safe argument handling
#

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

log_error() {
    echo -e "${RED}[ERROR]${NC} $*" >&2
}

log_info() {
    echo -e "${GREEN}[INFO]${NC} $*" >&2
}

# Authenticate with API key
auth_key() {
    local api_key="$1"

    if [[ -z "$api_key" ]]; then
        log_error "API key cannot be empty"
        return 1
    fi

    log_info "Authenticating with FormatFerry..."

    # Safe invocation - no eval
    if command -v formatferry &> /dev/null; then
        formatferry auth --api-key "$api_key"
    else
        npx formatferry auth --api-key "$api_key"
    fi
}

# Check auth status
auth_status() {
    log_info "Checking authentication status..."

    if command -v formatferry &> /dev/null; then
        formatferry auth --status
    else
        npx formatferry auth --status
    fi
}

main() {
    if [[ $# -eq 0 ]]; then
        log_error "Usage: $0 --key <api_key> | --status"
        return 1
    fi

    case "$1" in
        --key)
            if [[ -z "${2:-}" ]]; then
                log_error "API key required"
                return 1
            fi
            auth_key "$2"
            ;;
        --status)
            auth_status
            ;;
        *)
            log_error "Unknown option: $1"
            log_error "Usage: $0 --key <api_key> | --status"
            return 1
            ;;
    esac
}

main "$@"


path: scripts/convert-to-md.sh
content: #!/usr/bin/env bash
#
# FormatFerry Convert to Markdown Script
# Secure wrapper - NO eval, bash arrays, streaming output, robust path sanitization
#

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }

check_dependencies() {
    if ! command -v formatferry &> /dev/null && ! command -v npx &> /dev/null; then
        log_error "FormatFerry not found. Install: npm install -g formatferry"
        return 1
    fi
}

# ROBUST: Recursive path traversal sanitization
sanitize_path() {
    local path="$1"
    local cleaned="$path"
    
    # Loop until no more traversal patterns (handles ....//, ..../, ./, /../)
    while [[ "$cleaned" =~ \.\.(\/) || "$cleaned" =~ \.\.+(\/) || "$cleaned" =~ \/.*\.\. ]]; do
        cleaned=$(echo "$cleaned" | sed -E 's|/[.]+|/|g' | sed -E 's|^[.]+/||' | sed -E 's|^[/.]*$||')
    done
    
    # Handle encoded bypass (%2e%2e)
    cleaned=$(echo "$cleaned" | sed -E 's:#%2e%2e:#%00:gI' | sed -E 's:#%2e:/:gI')
    
    # Final check
    [[ "$cleaned" =~ \.\. ]] && return 1
    
    printf '%s' "$cleaned"
}

# Validate path is safe
validate_safe_path() {
    local path="$1"
    
    [[ -z "$path" ]] && return 1
    
    # Block sensitive absolute paths
    [[ "$path" =~ ^/etc ]] && return 1
    [[ "$path" =~ ^/root ]] && return 1
    [[ "$path" =~ ^/sys ]] && return 1
    [[ "$path" =~ ^/proc ]] && return 1
    
    # Block sensitive filenames
    local bn
    bn=$(basename "$path")
    [[ "$bn" == "passwd" ]] && return 1
    [[ "$bn" == "shadow" ]] && return 1
    [[ "$bn" == "id_rsa" ]] && return 1
    [[ "$bn" == ".bashrc" ]] && return 1
    [[ "$bn" == ".bash_history" ]] && return 1
    
    return 0
}

convert_to_md() {
    local INPUT_FILE=""
    local URL=""
    local OUTPUT_FILE=""
    local FORMAT="github"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --input|-i)
                INPUT_FILE="$2"
                shift 2
                ;;
            --url)
                URL="$2"
                shift 2
                ;;
            --output|-o)
                OUTPUT_FILE="$2"
                shift 2
                ;;
            --format|-f)
                FORMAT="$2"
                shift 2
                ;;
            *)
                log_error "Unknown option: $1"
                return 1
                ;;
        esac
    done

    if [[ -z "$INPUT_FILE" ]] && [[ -z "$URL" ]]; then
        log_error "Either --input or --url is required"
        return 1
    fi

    # Validate INPUT_FILE
    if [[ -n "$INPUT_FILE" ]]; then
        validate_safe_path "$INPUT_FILE" || {
            log_error "Input path is not allowed: $INPUT_FILE"
            return 1
        }
        
        local sanitized_input
        sanitized_input=$(sanitize_path "$INPUT_FILE") || {
            log_error "Input contains invalid traversal: $INPUT_FILE"
            return 1
        }
        
        if [[ ! -f "$INPUT_FILE" ]]; then
            log_error "Input file not found: $INPUT_FILE"
            return 1
        fi
    fi

    if [[ -n "$URL" ]] && [[ ! "$URL" =~ ^https?:// ]]; then
        log_error "Invalid URL (must start with http:// or https://)"
        return 1
    fi

    # Validate OUTPUT_FILE
    if [[ -n "$OUTPUT_FILE" ]]; then
        validate_safe_path "$OUTPUT_FILE" || {
            log_error "Output path is not allowed: $OUTPUT_FILE"
            return 1
        }
        
        local sanitized_output
        sanitized_output=$(sanitize_path "$OUTPUT_FILE") || {
            log_error "Output contains invalid traversal: $OUTPUT_FILE"
            return 1
        }
        
        OUTPUT_FILE="$sanitized_output"
    else
        if [[ -n "$INPUT_FILE" ]]; then
            OUTPUT_FILE="$(basename "$INPUT_FILE" | sed 's/\.[^.]*$//').md"
        else
            OUTPUT_FILE="converted-$(date +%s).md"
        fi
    fi

    # Ensure output in /tmp or current dir
    if [[ ! "$OUTPUT_FILE" =~ ^/ ]]; then
        OUTPUT_FILE="/tmp/$OUTPUT_FILE"
    fi

    local ARGS=()
    if [[ -n "$URL" ]]; then
        ARGS+=("--url" "$URL")
    else
        ARGS+=("-i" "$INPUT_FILE")
    fi
    ARGS+=("-o" "$OUTPUT_FILE")
    ARGS+=("-f" "$FORMAT")

    log_info "Converting..."
    
    if command -v formatferry &> /dev/null; then
        formatferry "${ARGS[@]}"
    else
        npx formatferry "${ARGS[@]}"
    fi

    log_info "Output: $OUTPUT_FILE"
    cat "$OUTPUT_FILE"
}

check_dependencies || exit 1
convert_to_md "$@"

path: scripts/convert.sh
content: #!/usr/bin/env bash
#
# FormatFerry Convert Script
# Secure wrapper - no eval, bash arrays, robust path sanitization
#

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

check_formatferry() {
    if ! command -v formatferry &> /dev/null && ! command -v npx &> /dev/null; then
        log_error "FormatFerry not found. Install: npm install -g formatferry"
        return 1
    fi
}

# Recursive path traversal sanitization
sanitize_path() {
    local path="$1"
    local cleaned="$path"
    
    while [[ "$cleaned" =~ \.\.(\/) || "$cleaned" =~ \.\.+(\/) || "$cleaned" =~ \/.*\.\. ]]; do
        cleaned=$(echo "$cleaned" | sed -E 's|/[.]+|/|g' | sed -E 's|^[.]+/||' | sed -E 's|^[/.]*$||')
    done
    
    cleaned=$(echo "$cleaned" | sed -E 's:#%2e%2e:#%00:gI' | sed -E 's:#%2e:/:gI')
    [[ "$cleaned" =~ \.\. ]] && return 1
    
    printf '%s' "$cleaned"
}

validate_safe_path() {
    local path="$1"
    [[ -z "$path" ]] && return 1
    
    [[ "$path" =~ ^/etc ]] && return 1
    [[ "$path" =~ ^/root ]] && return 1
    [[ "$path" =~ ^/sys ]] && return 1
    [[ "$path" =~ ^/proc ]] && return 1
    
    local bn
    bn=$(basename "$path")
    [[ "$bn" == "passwd" ]] && return 1
    [[ "$bn" == "shadow" ]] && return 1
    [[ "$bn" == "id_rsa" ]] && return 1
    [[ "$bn" == ".bashrc" ]] && return 1
    
    return 0
}

convert_file() {
    local input_file="$1"
    local output_file="${2:-}"
    local format="${3:-github}"

    validate_safe_path "$input_file" || {
        log_error "Input path not allowed: $input_file"
        return 1
    }
    
    [[ ! -f "$input_file" ]] && {
        log_error "Input file not found: $input_file"
        return 1
    }

    if [[ -z "$output_file" ]]; then
        output_file="$(basename "$input_file" | sed 's/\.[^.]*$//').md"
    fi

    validate_safe_path "$output_file" || {
        log_error "Output path not allowed: $output_file"
        return 1
    }
    
    local sanitized
    sanitized=$(sanitize_path "$output_file") || {
        log_error "Output contains traversal: $output_file"
        return 1
    }
    
    output_file="$sanitized"
    [[ ! "$output_file" =~ ^/ ]] && output_file="/tmp/$output_file"

    local ARGS=()
    ARGS+=("-i" "$input_file")
    ARGS+=("-o" "$output_file")
    ARGS+=("-f" "$format")

    log_info "Converting: $input_file → $output_file"

    if command -v formatferry &> /dev/null; then
        formatferry "${ARGS[@]}"
    else
        npx formatferry "${ARGS[@]}"
    fi

    log_info "Done: $output_file"
    echo "$output_file"
}

convert_url() {
    local url="$1"
    local output_file="${2:-}"
    local format="${3:-github}"

    [[ ! "$url" =~ ^https?:// ]] && {
        log_error "Invalid URL: $url"
        return 1
    }

    if [[ -z "$output_file" ]]; then
        output_file="converted-$(date +%s).md"
    fi

    validate_safe_path "$output_file" || {
        log_error "Output path not allowed: $output_file"
        return 1
    }
    
    local sanitized
    sanitized=$(sanitize_path "$output_file") || {
        log_error "Output contains traversal: $output_file"
        return 1
    }
    
    output_file="$sanitized"
    [[ ! "$output_file" =~ ^/ ]] && output_file="/tmp/$output_file"

    local ARGS=()
    ARGS+=("--url" "$url")
    ARGS+=("--output" "$output_file")
    ARGS+=("-f" "$format")

    log_info "Fetching: $url → $output_file"

    if command -v formatferry &> /dev/null; then
        formatferry "${ARGS[@]}"
    else
        npx formatferry "${ARGS[@]}"
    fi

    log_info "Done: $output_file"
    echo "$output_file"
}

main() {
    [[ $# -eq 0 ]] && {
        log_error "Usage: $0 --file <input> [--output <file>] [--format <format>]"
        log_error "   or: $0 --url <url> [--output <file>] [--format <format>]"
        return 1
    }

    check_formatferry || return 1

    local mode="" input="" output="" format="github"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --file) mode="file"; input="$2"; shift 2 ;;
            --url) mode="url"; input="$2"; shift 2 ;;
            --output) output="$2"; shift 2 ;;
            --format) format="$2"; shift 2 ;;
            *) log_error "Unknown: $1"; return 1 ;;
        esac
    done

    [[ -z "$mode" ]] || [[ -z "$input" ]] && {
        log_error "Missing required argument"
        return 1
    }

    case "$mode" in
        file) convert_file "$input" "$output" "$format" ;;
        url) convert_url "$input" "$output" "$format" ;;
        *) log_error "Invalid mode: $mode"; return 1 ;;
    esac
}

main "$@"

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)