mirror of
https://github.com/lunchcat/sif.git
synced 2026-01-13 13:27:30 -08:00
adds a new --shodan flag that queries the shodan api for information about the target host. requires SHODAN_API_KEY environment variable. features: - resolves hostnames to ip addresses - queries shodan host api for reconnaissance data - displays organization, isp, location, ports, services, and vulns - logs results to file when logdir is specified closes #2
181 lines
5.5 KiB
Go
181 lines
5.5 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2025 vmfunc (Celeste Hickenlooper), xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package scan
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestResolveHostname_IP(t *testing.T) {
|
|
ip, err := resolveHostname("8.8.8.8")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ip != "8.8.8.8" {
|
|
t.Errorf("expected '8.8.8.8', got '%s'", ip)
|
|
}
|
|
}
|
|
|
|
func TestResolveHostname_Hostname(t *testing.T) {
|
|
ip, err := resolveHostname("localhost")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ip != "127.0.0.1" && ip != "::1" {
|
|
t.Errorf("expected localhost to resolve to 127.0.0.1 or ::1, got '%s'", ip)
|
|
}
|
|
}
|
|
|
|
func TestTruncateBanner(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
maxLen int
|
|
expected string
|
|
}{
|
|
{"short", 10, "short"},
|
|
{"this is a long banner", 10, "this is a ..."},
|
|
{"with\nnewlines\r\n", 50, "with newlines"},
|
|
{" trimmed ", 50, "trimmed"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := truncateBanner(tt.input, tt.maxLen)
|
|
if result != tt.expected {
|
|
t.Errorf("truncateBanner(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQueryShodanHost_NotFound(t *testing.T) {
|
|
// this test verifies that a mock server returning 404 is handled correctly
|
|
// note: we can't easily override the const shodanBaseURL for testing
|
|
// so this is more of a documentation of expected behavior
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// the actual API query would return a partial result with just the IP
|
|
// when Shodan has no data for a host
|
|
}
|
|
|
|
func TestQueryShodanHost_Success(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
response := shodanHostResponse{
|
|
IP: "93.184.216.34",
|
|
Hostnames: []string{"example.com"},
|
|
Org: "EDGECAST",
|
|
ASN: "AS15133",
|
|
ISP: "Edgecast Inc.",
|
|
CountryName: "United States",
|
|
City: "Los Angeles",
|
|
Ports: []int{80, 443},
|
|
Data: []shodanData{
|
|
{
|
|
Port: 80,
|
|
Transport: "tcp",
|
|
Product: "nginx",
|
|
Version: "1.18.0",
|
|
Data: "HTTP/1.1 200 OK\r\nServer: nginx",
|
|
},
|
|
},
|
|
}
|
|
json.NewEncoder(w).Encode(response)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Note: This test would need the actual API endpoint to be overridable
|
|
// For now, we just verify the response parsing
|
|
}
|
|
|
|
func TestShodanResult_Fields(t *testing.T) {
|
|
result := ShodanResult{
|
|
IP: "93.184.216.34",
|
|
Hostnames: []string{"example.com"},
|
|
Organization: "EDGECAST",
|
|
ASN: "AS15133",
|
|
ISP: "Edgecast Inc.",
|
|
Country: "United States",
|
|
City: "Los Angeles",
|
|
Ports: []int{80, 443},
|
|
Services: []ShodanService{
|
|
{
|
|
Port: 80,
|
|
Protocol: "tcp",
|
|
Product: "nginx",
|
|
Version: "1.18.0",
|
|
},
|
|
},
|
|
}
|
|
|
|
if result.IP != "93.184.216.34" {
|
|
t.Errorf("expected IP '93.184.216.34', got '%s'", result.IP)
|
|
}
|
|
if len(result.Hostnames) != 1 || result.Hostnames[0] != "example.com" {
|
|
t.Errorf("expected hostnames ['example.com'], got %v", result.Hostnames)
|
|
}
|
|
if result.Organization != "EDGECAST" {
|
|
t.Errorf("expected org 'EDGECAST', got '%s'", result.Organization)
|
|
}
|
|
if len(result.Ports) != 2 {
|
|
t.Errorf("expected 2 ports, got %d", len(result.Ports))
|
|
}
|
|
if len(result.Services) != 1 {
|
|
t.Errorf("expected 1 service, got %d", len(result.Services))
|
|
}
|
|
}
|
|
|
|
func TestShodanService_Fields(t *testing.T) {
|
|
service := ShodanService{
|
|
Port: 443,
|
|
Protocol: "tcp",
|
|
Product: "OpenSSL",
|
|
Version: "1.1.1",
|
|
Banner: "TLS handshake",
|
|
Module: "https",
|
|
}
|
|
|
|
if service.Port != 443 {
|
|
t.Errorf("expected port 443, got %d", service.Port)
|
|
}
|
|
if service.Protocol != "tcp" {
|
|
t.Errorf("expected protocol 'tcp', got '%s'", service.Protocol)
|
|
}
|
|
if service.Product != "OpenSSL" {
|
|
t.Errorf("expected product 'OpenSSL', got '%s'", service.Product)
|
|
}
|
|
}
|
|
|
|
func TestShodan_NoAPIKey(t *testing.T) {
|
|
// ensure no API key is set
|
|
originalKey := ""
|
|
// Note: we can't easily test this without setting/unsetting env vars
|
|
// which could affect other tests. This is just a placeholder.
|
|
_ = originalKey
|
|
}
|
|
|
|
func TestShodanIntegration(t *testing.T) {
|
|
// This would be an integration test with the real Shodan API
|
|
// Skipping in unit tests
|
|
t.Skip("Integration test - requires valid SHODAN_API_KEY")
|
|
|
|
_, err := Shodan("https://example.com", 10*time.Second, "")
|
|
if err != nil {
|
|
t.Logf("Shodan lookup failed (expected without API key): %v", err)
|
|
}
|
|
}
|