Skip to content

feat: Adding federated identity credentials#164

Open
hotnops wants to merge 4 commits intoSpecterOps:mainfrom
hotnops:federated-identity-credentials
Open

feat: Adding federated identity credentials#164
hotnops wants to merge 4 commits intoSpecterOps:mainfrom
hotnops:federated-identity-credentials

Conversation

@hotnops
Copy link

@hotnops hotnops commented Jan 28, 2026

Adding collection of federated identity credentials to the azurehound collector.

Summary by CodeRabbit

  • New Features

    • Added CLI support to list Federated Identity Credentials for Azure AD applications, integrated into the aggregated listing output for comprehensive app data.
    • Results now include structured JSON representations for per-app FIC entries.
  • Tests

    • Expanded mocks to cover the new listing operation, improving testability of the new feature.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Walkthrough

Adds listing support for Azure AD Application Federated Identity Credentials: client method, interface and mock updates, new models and enum, a Cobra CLI command to stream FICs with concurrent workers, and integration into the existing list pipeline.

Changes

Cohort / File(s) Summary
Client Interface & Implementation
client/client.go, client/apps.go
Added ListAzureADAppFICs to AzureGraphClient and azureClient; builds Graph path for /federatedIdentityCredentials, defaults top=99 if unset, and streams results via getAzureObjectList goroutine.
Mocks
client/mocks/client.go
Added mock method and recorder for ListAzureADAppFICs following existing gomock patterns.
Data Models & Enum
models/app-fic.go, enums/kind.go
New AppFIC and AppFICs types with custom MarshalJSON; added KindAZFederatedIdentityCredential constant.
CLI Command & Pipeline Integration
cmd/list-app-federated-identity-credentials.go, cmd/list-azure-ad.go
New Cobra command list apps pipeline listAppFICs that concurrently enumerates FICs per app and emits azureWrapper[models.AppFICs]; increased tee from 2→3 and wired new stream into multiplexed output.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Command
    participant Client as Azure Client
    participant API as Azure Graph API
    participant Pipeline as Processing Pipeline

    CLI->>Client: create client, ctx
    CLI->>Pipeline: start listAppFICs workers
    Pipeline->>Client: ListAzureADAppFICs(appId, params)
    Client->>API: GET /applications/{appId}/federatedIdentityCredentials
    API-->>Client: returns FIC items (paged)
    Client-->>Pipeline: stream AzureResult[json.RawMessage] via channel
    Pipeline->>Pipeline: accumulate into models.AppFICs, wrap in azureWrapper
    Pipeline-->>CLI: emit to output channel
    Pipeline-->>CLI: close channel when workers complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code and fetched each key,

FICs in paws, a tiny jubilee.
Apps and workers danced in line,
Streaming treasures, one by one—how fine!
A carrot-toast to pipelines new and spry.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding federated identity credentials collection to the Azure client. It aligns with the PR objectives and file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@client/apps.go`:
- Around line 63-76: The path for the federatedIdentityCredentials endpoint in
ListAzureADAppFICs currently includes a trailing slash; remove the trailing
slash from the formatted path string in function ListAzureADAppFICs (the
variable path built with fmt.Sprintf) so it matches the other Graph calls and
Microsoft docs (use "/%s/applications/%s/federatedIdentityCredentials" instead
of the version that ends with "/").

In `@cmd/list-app-federated-identity-credentials.go`:
- Around line 84-87: The log message inside the loop over
client.ListAzureADAppFICs(ctx, app.Data.Id, params) is misleadingly referencing
“owners”; update the process so that when item.Error != nil the processLogger
(or log) message reflects Federated Identity Credentials (FIC) collection
instead—e.g., change "unable to continue processing owners for this app" to
something like "unable to continue processing federated identity credentials for
this app" or "unable to process FICs for this app", keeping the existing fields
("appId", app.Data.AppId) and the Error payload; locate the loop over
ListAzureADAppFICs and replace only the message text.

In `@models/app-fic.go`:
- Around line 29-38: The custom MarshalJSON in AppFIC calls OmitEmpty on s.FIC
which errors on nil/empty json.RawMessage; update AppFIC.MarshalJSON to first
check s.FIC (e.g., len(s.FIC) == 0 or s.FIC == nil) and if empty skip calling
OmitEmpty and do not set "fic" in the output map, otherwise call OmitEmpty on
s.FIC and set output["fic"] from its result before json.Marshal(output); keep
the rest of the method (setting "appId" and returning json.Marshal(output))
unchanged.

Comment on lines 29 to 38
func (s *AppFIC) MarshalJSON() ([]byte, error) {
output := make(map[string]any)
output["appId"] = s.AppId

if fic, err := OmitEmpty(s.FIC); err != nil {
return nil, err
} else {
output["fic"] = fic
return json.Marshal(output)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard empty FIC to avoid marshal errors.

OmitEmpty will error on empty/nil json.RawMessage. If FIC is optional, skip the call when empty and only include the field when present.

🛠️ Proposed fix
 func (s *AppFIC) MarshalJSON() ([]byte, error) {
 	output := make(map[string]any)
 	output["appId"] = s.AppId
 
+	if len(s.FIC) == 0 {
+		return json.Marshal(output)
+	}
+
 	if fic, err := OmitEmpty(s.FIC); err != nil {
 		return nil, err
 	} else {
 		output["fic"] = fic
 		return json.Marshal(output)
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *AppFIC) MarshalJSON() ([]byte, error) {
output := make(map[string]any)
output["appId"] = s.AppId
if fic, err := OmitEmpty(s.FIC); err != nil {
return nil, err
} else {
output["fic"] = fic
return json.Marshal(output)
}
func (s *AppFIC) MarshalJSON() ([]byte, error) {
output := make(map[string]any)
output["appId"] = s.AppId
if len(s.FIC) == 0 {
return json.Marshal(output)
}
if fic, err := OmitEmpty(s.FIC); err != nil {
return nil, err
} else {
output["fic"] = fic
return json.Marshal(output)
}
}
🤖 Prompt for AI Agents
In `@models/app-fic.go` around lines 29 - 38, The custom MarshalJSON in AppFIC
calls OmitEmpty on s.FIC which errors on nil/empty json.RawMessage; update
AppFIC.MarshalJSON to first check s.FIC (e.g., len(s.FIC) == 0 or s.FIC == nil)
and if empty skip calling OmitEmpty and do not set "fic" in the output map,
otherwise call OmitEmpty on s.FIC and set output["fic"] from its result before
json.Marshal(output); keep the rest of the method (setting "appId" and returning
json.Marshal(output)) unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@cmd/list-app-federated-identity-credentials.go`:
- Around line 20-25: The code registers signals using os.Kill which is
uncatchable; update the signal handling to use syscall.SIGTERM instead of
os.Kill so graceful shutdown works in containers: replace any use of os.Kill
passed to signal.Notify (e.g., signal.Notify(sigCh, os.Interrupt, os.Kill)) with
syscall.SIGTERM (e.g., signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)) and
add the syscall import; ensure the same change is applied to the other
occurrence around lines 48-50 referencing signal.Notify or os.Kill.

Comment on lines +20 to +25
import (
"context"
"os"
"os/signal"
"sync"
"time"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Go os.Kill SIGKILL cannot be trapped caught signal.NotifyContext

💡 Result:

On Unix-like systems, SIGKILL (and SIGSTOP) cannot be caught, blocked, or ignored, so Go cannot “trap” it—neither signal.Notify nor signal.NotifyContext will ever deliver SIGKILL to your program for graceful shutdown. The Go os/signal docs state this explicitly. [1] (This is an OS/POSIX rule, not a Go limitation. [2])

Also, in Go os.Kill is SIGKILL (and (*os.Process).Kill() “causes the Process to exit immediately”), so anything that sends os.Kill/Process.Kill() is inherently non-graceful. [3]

What to do instead (graceful shutdown)

Use a catchable signal like SIGINT/SIGTERM:

ctx, stop := signal.NotifyContext(context.Background(),
    os.Interrupt, syscall.SIGTERM,
)
defer stop()

<-ctx.Done() // start shutdown/cleanup

Then, if you need a “hard stop” fallback, do it from outside the process (service manager / supervisor): send SIGTERM, wait a grace period, then send SIGKILL.

Sources: Go os/signal docs on SIGKILL/SIGSTOP [1], POSIX signal rules (SIGKILL can’t be caught/ignored) [2], Go os docs showing Kill = SIGKILL / immediate exit semantics [3].

[1] https://pkg.go.dev/os/signal
[2] https://manual.cs50.io/7/signal
[3] https://pkg.go.dev/github.com/s1s1ty/go/src/os


🌐 Web query:

Go graceful shutdown SIGTERM syscall signal handling

💡 Result:

In Go, you typically implement “graceful shutdown” by catching SIGTERM/SIGINT and then cancelling work + calling (*http.Server).Shutdown(ctx) with a timeout.

Why this works

  • By default, SIGINT/SIGTERM terminate the program, but signal.Notify / signal.NotifyContext disable the default behavior and deliver the signal to your code so you can clean up. [1]
  • http.Server.Shutdown stops accepting new connections, closes listeners/idle conns, and waits for in-flight requests until the context expires. (It does not handle hijacked conns like WebSockets automatically.) [2]

Minimal pattern (recommended): signal.NotifyContext

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	srv := &http.Server{Addr: ":8080"}

	// Cancel this context when SIGINT (Ctrl+C) or SIGTERM is received.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %v", err)
		}
	}()

	<-ctx.Done() // wait for signal

	// Give outstanding requests time to finish.
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Printf("shutdown: %v", err)
	}
}

Notes

  • In containers/orchestrators (Docker/systemd/Kubernetes), SIGTERM is the common “please stop” signal, so include it. [3]
  • You can’t gracefully handle SIGKILL (it cannot be caught/ignored); only SIGTERM (then possibly a later SIGKILL if you don’t exit in time).

Sources:
[1] Go os/signal docs (signal.Notify behavior, incl. SIGTERM) https://go.dev/src/os/signal/doc.go
[2] Example + http.Server.Shutdown behavior notes (incl. WebSockets/hijacked conns) https://rafallorenz.com/go/handle-signals-to-graceful-shutdown-http-server/
[3] Typical use of SIGTERM for shutdown (e.g., Docker/systemd/K8s) https://m.php.cn/en/faq/1796844688.html


🏁 Script executed:

# Check the actual implementation at the mentioned lines
head -60 cmd/list-app-federated-identity-credentials.go | tail -45

Repository: SpecterOps/AzureHound

Length of output: 1403


Replace os.Kill with syscall.SIGTERM for proper graceful shutdown in containerized environments.

os.Kill (SIGKILL) cannot be trapped—the OS prevents it. Without SIGTERM, graceful shutdown fails when orchestrators (Docker, Kubernetes, systemd) send the standard termination signal. Only os.Interrupt (SIGINT) will trigger cleanup, which won't occur during normal container shutdown.

Proposed fix
 import (
 	"context"
 	"os"
 	"os/signal"
 	"sync"
+	"syscall"
 	"time"
-	ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill)
+	ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)

Also applies to: 48-50

🤖 Prompt for AI Agents
In `@cmd/list-app-federated-identity-credentials.go` around lines 20 - 25, The
code registers signals using os.Kill which is uncatchable; update the signal
handling to use syscall.SIGTERM instead of os.Kill so graceful shutdown works in
containers: replace any use of os.Kill passed to signal.Notify (e.g.,
signal.Notify(sigCh, os.Interrupt, os.Kill)) with syscall.SIGTERM (e.g.,
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)) and add the syscall import;
ensure the same change is applied to the other occurrence around lines 48-50
referencing signal.Notify or os.Kill.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant