007
無料Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.
日本語の概要は準備中です。原文の説明を表示しています。
Generates audit trail infrastructure for entities. Implements IAuditable interface, EF Core SaveChanges interceptor, and automatic population of CreatedAt, UpdatedAt, CreatedBy, and UpdatedBy fields.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Automatic audit trail tracking for entities:
| Component | Purpose |
|---|---|
IAuditable | Interface for auditable entities |
AuditableEntity | Base class with audit fields |
AuditSaveChangesInterceptor | Auto-populates audit fields |
SoftDeletable | Interface for soft delete |
/Domain/Abstractions/
├── IAuditable.cs
├── ISoftDeletable.cs
└── AuditableEntity.cs
/Infrastructure/
├── Interceptors/
│ └── AuditSaveChangesInterceptor.cs
└── ApplicationDbContext.cs
// src/{name}.domain/Abstractions/IAuditable.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Interface for entities that track creation and modification metadata
/// </summary>
public interface IAuditable
{
/// <summary>
/// UTC timestamp when the entity was created
/// </summary>
DateTime CreatedAtUtc { get; }
/// <summary>
/// ID of the user who created the entity
/// </summary>
Guid? CreatedBy { get; }
/// <summary>
/// UTC timestamp when the entity was last modified
/// </summary>
DateTime? UpdatedAtUtc { get; }
/// <summary>
/// ID of the user who last modified the entity
/// </summary>
Guid? UpdatedBy { get; }
}
// src/{name}.domain/Abstractions/ISoftDeletable.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Interface for entities that support soft delete
/// </summary>
public interface ISoftDeletable
{
/// <summary>
/// Whether the entity has been soft deleted
/// </summary>
bool IsDeleted { get; }
/// <summary>
/// UTC timestamp when the entity was deleted
/// </summary>
DateTime? DeletedAtUtc { get; }
/// <summary>
/// ID of the user who deleted the entity
/// </summary>
Guid? DeletedBy { get; }
}
// src/{name}.domain/Abstractions/AuditableEntity.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Base class for entities that track audit information
/// </summary>
public abstract class AuditableEntity : Entity, IAuditable, ISoftDeletable
{
// ═══════════════════════════════════════════════════════════════
// AUDIT FIELDS (IAuditable)
// ═══════════════════════════════════════════════════════════════
public DateTime CreatedAtUtc { get; private set; }
public Guid? CreatedBy { get; private set; }
public DateTime? UpdatedAtUtc { get; private set; }
public Guid? UpdatedBy { get; private set; }
// ═══════════════════════════════════════════════════════════════
// SOFT DELETE FIELDS (ISoftDeletable)
// ═══════════════════════════════════════════════════════════════
public bool IsDeleted { get; private set; }
public DateTime? DeletedAtUtc { get; private set; }
public Guid? DeletedBy { get; private set; }
protected AuditableEntity() : base()
{
}
protected AuditableEntity(Guid id) : base(id)
{
}
// ═══════════════════════════════════════════════════════════════
// AUDIT METHODS (called by interceptor or manually)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Sets creation audit fields. Called automatically by interceptor.
/// </summary>
internal void SetCreatedAudit(DateTime utcNow, Guid? userId)
{
CreatedAtUtc = utcNow;
CreatedBy = userId;
}
/// <summary>
/// Sets modification audit fields. Called automatically by interceptor.
/// </summary>
internal void SetModifiedAudit(DateTime utcNow, Guid? userId)
{
UpdatedAtUtc = utcNow;
UpdatedBy = userId;
}
/// <summary>
/// Soft deletes the entity
/// </summary>
public virtual void SoftDelete(DateTime utcNow, Guid? userId)
{
if (IsDeleted)
{
return;
}
IsDeleted = true;
DeletedAtUtc = utcNow;
DeletedBy = userId;
}
/// <summary>
/// Restores a soft-deleted entity
/// </summary>
public virtual void Restore()
{
IsDeleted = false;
DeletedAtUtc = null;
DeletedBy = null;
}
}
// src/{name}.infrastructure/Interceptors/AuditSaveChangesInterceptor.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using {name}.application.abstractions.authentication;
using {name}.domain.abstractions;
namespace {name}.infrastructure.interceptors;
/// <summary>
/// Interceptor that automatically populates audit fields on SaveChanges
/// </summary>
public sealed class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
private readonly IUserContext _userContext;
public AuditSaveChangesInterceptor(IUserContext userContext)
{
_userContext = userContext;
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is not null)
{
UpdateAuditFields(eventData.Context);
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
if (eventData.Context is not null)
{
UpdateAuditFields(eventData.Context);
}
return base.SavingChanges(eventData, result);
}
private void UpdateAuditFields(DbContext context)
{
var utcNow = DateTime.UtcNow;
var userId = GetCurrentUserId();
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
SetCreatedAudit(entry, utcNow, userId);
break;
case EntityState.Modified:
SetModifiedAudit(entry, utcNow, userId);
break;
}
}
// Handle soft delete
foreach (var entry in context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
// Convert hard delete to soft delete
entry.State = EntityState.Modified;
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SoftDelete(utcNow, userId);
}
}
}
}
private void SetCreatedAudit(EntityEntry<IAuditable> entry, DateTime utcNow, Guid? userId)
{
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SetCreatedAudit(utcNow, userId);
}
else
{
// For entities implementing IAuditable but not inheriting AuditableEntity
entry.Property(nameof(IAuditable.CreatedAtUtc)).CurrentValue = utcNow;
entry.Property(nameof(IAuditable.CreatedBy)).CurrentValue = userId;
}
}
private void SetModifiedAudit(EntityEntry<IAuditable> entry, DateTime utcNow, Guid? userId)
{
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SetModifiedAudit(utcNow, userId);
}
else
{
entry.Property(nameof(IAuditable.UpdatedAtUtc)).CurrentValue = utcNow;
entry.Property(nameof(IAuditable.UpdatedBy)).CurrentValue = userId;
}
}
private Guid? GetCurrentUserId()
{
try
{
return _userContext.IsAuthenticated ? _userContext.UserId : null;
}
catch
{
// User context may not be available in background jobs
return null;
}
}
}
// src/{name}.infrastructure/Configurations/AuditableEntityConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.abstractions;
namespace {name}.infrastructure.configurations;
/// <summary>
/// Base configuration for auditable entities
/// Apply using: builder.ApplyConfiguration(new AuditableEntityConfiguration<YourEntity>());
/// </summary>
public class AuditableEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
where TEntity : AuditableEntity
{
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
// ═══════════════════════════════════════════════════════════════
// AUDIT FIELDS
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.CreatedAtUtc)
.HasColumnName("created_at_utc")
.IsRequired();
builder.Property(e => e.CreatedBy)
.HasColumnName("created_by");
builder.Property(e => e.UpdatedAtUtc)
.HasColumnName("updated_at_utc");
builder.Property(e => e.UpdatedBy)
.HasColumnName("updated_by");
// ═══════════════════════════════════════════════════════════════
// SOFT DELETE FIELDS
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.IsDeleted)
.HasColumnName("is_deleted")
.HasDefaultValue(false)
.IsRequired();
builder.Property(e => e.DeletedAtUtc)
.HasColumnName("deleted_at_utc");
builder.Property(e => e.DeletedBy)
.HasColumnName("deleted_by");
// ═══════════════════════════════════════════════════════════════
// GLOBAL QUERY FILTER (Soft Delete)
// ═══════════════════════════════════════════════════════════════
builder.HasQueryFilter(e => !e.IsDeleted);
// ═══════════════════════════════════════════════════════════════
// INDEXES
// ═══════════════════════════════════════════════════════════════
builder.HasIndex(e => e.CreatedAtUtc)
.HasDatabaseName($"ix_{typeof(TEntity).Name.ToLower()}_created_at");
builder.HasIndex(e => e.IsDeleted)
.HasDatabaseName($"ix_{typeof(TEntity).Name.ToLower()}_is_deleted")
.HasFilter("is_deleted = true");
}
}
// src/{name}.domain/Users/User.cs
using {name}.domain.abstractions;
using {name}.domain.users.events;
namespace {name}.domain.users;
public sealed class User : AuditableEntity
{
public string Name { get; private set; } = string.Empty;
public Email Email { get; private set; } = null!;
public Guid OrganizationId { get; private set; }
public bool IsActive { get; private set; }
private User() : base()
{
}
private User(
Guid id,
string name,
Email email,
Guid organizationId)
: base(id)
{
Name = name;
Email = email;
OrganizationId = organizationId;
IsActive = true;
}
public static Result<User> Create(
string name,
string email,
Guid organizationId)
{
var emailResult = Email.Create(email);
if (emailResult.IsFailure)
{
return Result.Failure<User>(emailResult.Error);
}
var user = new User(
Guid.NewGuid(),
name,
emailResult.Value,
organizationId);
user.RaiseDomainEvent(new UserCreatedDomainEvent(user.Id));
return user;
// CreatedAtUtc and CreatedBy will be set automatically by interceptor
}
public Result UpdateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure(UserErrors.NameRequired);
}
Name = name;
// UpdatedAtUtc and UpdatedBy will be set automatically by interceptor
return Result.Success();
}
public void Deactivate()
{
IsActive = false;
RaiseDomainEvent(new UserDeactivatedDomainEvent(Id));
}
}
// src/{name}.infrastructure/Configurations/UserConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.users;
namespace {name}.infrastructure.configurations;
internal sealed class UserConfiguration : AuditableEntityConfiguration<User>
{
public override void Configure(EntityTypeBuilder<User> builder)
{
// Call base configuration for audit fields
base.Configure(builder);
builder.ToTable("users");
builder.HasKey(u => u.Id);
builder.Property(u => u.Name)
.HasColumnName("name")
.HasMaxLength(100)
.IsRequired();
builder.OwnsOne(u => u.Email, emailBuilder =>
{
emailBuilder.Property(e => e.Value)
.HasColumnName("email")
.HasMaxLength(255)
.IsRequired();
emailBuilder.HasIndex(e => e.Value)
.IsUnique();
});
builder.Property(u => u.OrganizationId)
.HasColumnName("organization_id")
.IsRequired();
builder.Property(u => u.IsActive)
.HasColumnName("is_active")
.HasDefaultValue(true)
.IsRequired();
builder.HasIndex(u => u.OrganizationId)
.HasDatabaseName("ix_users_organization_id");
}
}
// src/{name}.infrastructure/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using {name}.domain.abstractions;
namespace {name}.infrastructure;
public sealed class ApplicationDbContext : DbContext, IUnitOfWork
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
}
// Registration in DependencyInjection.cs
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<ApplicationDbContext>((sp, options) =>
{
options.UseNpgsql(configuration.GetConnectionString("Database"));
options.AddInterceptors(sp.GetRequiredService<AuditSaveChangesInterceptor>());
});
services.AddScoped<AuditSaveChangesInterceptor>();
return services;
}
// src/{name}.domain/Auditing/AuditLog.cs
namespace {name}.domain.auditing;
/// <summary>
/// Stores complete audit history of changes
/// </summary>
public sealed class AuditLog
{
public Guid Id { get; set; }
public string EntityName { get; set; } = string.Empty;
public string EntityId { get; set; } = string.Empty;
public string Action { get; set; } = string.Empty; // Created, Modified, Deleted
public string? OldValues { get; set; } // JSON
public string? NewValues { get; set; } // JSON
public string? AffectedColumns { get; set; } // JSON array
public DateTime OccurredAtUtc { get; set; }
public Guid? UserId { get; set; }
public string? UserName { get; set; }
public string? IpAddress { get; set; }
}
// src/{name}.infrastructure/Interceptors/AuditLogInterceptor.cs
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using {name}.application.abstractions.authentication;
using {name}.domain.auditing;
namespace {name}.infrastructure.interceptors;
/// <summary>
/// Interceptor that logs complete change history to AuditLog table
/// </summary>
public sealed class AuditLogInterceptor : SaveChangesInterceptor
{
private readonly IUserContext _userContext;
public AuditLogInterceptor(IUserContext userContext)
{
_userContext = userContext;
}
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is ApplicationDbContext context)
{
await CreateAuditLogs(context, cancellationToken);
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
private async Task CreateAuditLogs(ApplicationDbContext context, CancellationToken ct)
{
context.ChangeTracker.DetectChanges();
var auditLogs = new List<AuditLog>();
var utcNow = DateTime.UtcNow;
var userId = _userContext.IsAuthenticated ? _userContext.UserId : (Guid?)null;
foreach (var entry in context.ChangeTracker.Entries())
{
if (entry.Entity is AuditLog ||
entry.State == EntityState.Detached ||
entry.State == EntityState.Unchanged)
{
continue;
}
var auditLog = new AuditLog
{
Id = Guid.NewGuid(),
EntityName = entry.Entity.GetType().Name,
EntityId = GetPrimaryKeyValue(entry),
OccurredAtUtc = utcNow,
UserId = userId
};
switch (entry.State)
{
case EntityState.Added:
auditLog.Action = "Created";
auditLog.NewValues = SerializeProperties(entry, p => p.CurrentValue);
break;
case EntityState.Modified:
auditLog.Action = "Modified";
auditLog.OldValues = SerializeProperties(entry, p => p.OriginalValue, true);
auditLog.NewValues = SerializeProperties(entry, p => p.CurrentValue, true);
auditLog.AffectedColumns = JsonSerializer.Serialize(
entry.Properties
.Where(p => p.IsModified)
.Select(p => p.Metadata.Name)
.ToList());
break;
case EntityState.Deleted:
auditLog.Action = "Deleted";
auditLog.OldValues = SerializeProperties(entry, p => p.OriginalValue);
break;
}
auditLogs.Add(auditLog);
}
if (auditLogs.Any())
{
await context.Set<AuditLog>().AddRangeAsync(auditLogs, ct);
}
}
private static string GetPrimaryKeyValue(EntityEntry entry)
{
var keyProperty = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey());
return keyProperty?.CurrentValue?.ToString() ?? "unknown";
}
private static string? SerializeProperties(
EntityEntry entry,
Func<PropertyEntry, object?> valueSelector,
bool onlyModified = false)
{
var properties = entry.Properties
.Where(p => !p.Metadata.IsPrimaryKey())
.Where(p => !onlyModified || p.IsModified)
.ToDictionary(
p => p.Metadata.Name,
p => valueSelector(p));
return properties.Any()
? JsonSerializer.Serialize(properties)
: null;
}
}
// When you need to include soft-deleted records:
public async Task<User?> GetByIdIncludingDeletedAsync(
Guid id,
CancellationToken cancellationToken)
{
return await _dbContext.Users
.IgnoreQueryFilters() // Include soft-deleted records
.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
}
// Query only deleted records:
public async Task<IReadOnlyList<User>> GetDeletedUsersAsync(
CancellationToken cancellationToken)
{
return await _dbContext.Users
.IgnoreQueryFilters()
.Where(u => u.IsDeleted)
.OrderByDescending(u => u.DeletedAtUtc)
.ToListAsync(cancellationToken);
}
-- Add audit columns to existing table
ALTER TABLE users
ADD COLUMN created_at_utc TIMESTAMP NOT NULL DEFAULT NOW(),
ADD COLUMN created_by UUID NULL,
ADD COLUMN updated_at_utc TIMESTAMP NULL,
ADD COLUMN updated_by UUID NULL,
ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN deleted_at_utc TIMESTAMP NULL,
ADD COLUMN deleted_by UUID NULL;
-- Create indexes
CREATE INDEX ix_users_created_at ON users (created_at_utc);
CREATE INDEX ix_users_is_deleted ON users (is_deleted) WHERE is_deleted = TRUE;
-- Audit log table (if using full history)
CREATE TABLE audit_log (
id UUID PRIMARY KEY,
entity_name VARCHAR(255) NOT NULL,
entity_id VARCHAR(255) NOT NULL,
action VARCHAR(50) NOT NULL,
old_values JSONB NULL,
new_values JSONB NULL,
affected_columns JSONB NULL,
occurred_at_utc TIMESTAMP NOT NULL,
user_id UUID NULL,
user_name VARCHAR(255) NULL,
ip_address VARCHAR(50) NULL
);
CREATE INDEX ix_audit_log_entity ON audit_log (entity_name, entity_id);
CREATE INDEX ix_audit_log_occurred_at ON audit_log (occurred_at_utc DESC);
CREATE INDEX ix_audit_log_user_id ON audit_log (user_id);
IgnoreQueryFilters() when needed// ❌ WRONG: Setting audit fields manually
user.CreatedAtUtc = DateTime.Now; // Let interceptor handle it!
// ✅ CORRECT: Interceptor handles automatically
var user = User.Create(...);
await _unitOfWork.SaveChangesAsync(); // Audit fields set by interceptor
// ❌ WRONG: Using local time
builder.Property(e => e.CreatedAtUtc)
.HasDefaultValueSql("NOW()"); // Could be local time!
// ✅ CORRECT: Explicitly UTC
builder.Property(e => e.CreatedAtUtc)
.HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'");
// ❌ WRONG: Forgetting query filters exist
var allUsers = await _dbContext.Users.ToListAsync(); // Missing deleted users might be intentional, but be aware!
// ✅ CORRECT: Explicit about including deleted
var allUsersIncludingDeleted = await _dbContext.Users
.IgnoreQueryFilters()
.ToListAsync();
domain-entity-generator - Entity base classesef-core-configuration - Entity configurationjwt-authentication - IUserContext for tracking userrepository-pattern - Repository methods with soft deleteまだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.
日本語の概要は準備中です。原文の説明を表示しています。
Guides the creation of agile user stories and Gherkin feature files. Use when the user wants to create a user story, write acceptance criteria, define Gherkin scenarios, or author BDD feature files. This should trigger for requests such as Create a user story; Write a user story; I need to write a user story. Part of cursor-rules-java project
日本語の概要は準備中です。原文の説明を表示しています。
Facilitates conversational discovery to create Architectural Decision Records (ADRs) for non-functional requirements using the ISO/IEC 25010:2023 quality model. Use when the user wants to document quality attributes, NFR decisions, security/performance/scalability architecture, or design systems with measurable quality criteria. This should trigger for requests such as Create ADR for Non-functional requirements; Document Non-functional requirements; Capture Non-functional requirements; Generate Non-functional requirements in an ADR. Part of cursor-rules-java project
日本語の概要は準備中です。原文の説明を表示しています。
Run a health check on an existing project: dependency audit, security scan, test runner detection, CI/CD evaluation, and missing configuration analysis. Maps the three execution gates (pre/in/post) from /10x-bootstrapper to an assessment framework for existing codebases. Reads optional context/foundation/stack-assessment.md from /10x-stack-assess to focus checks on identified gaps. Writes context/foundation/health-check.md with findings, prioritized fixes, and an agent-readiness verdict. Use when the user has an existing project and wants to verify its health before working with an agent. Trigger phrases: "health check", "check my project", "audit my project", "is my project healthy", "sprawdź projekt", "audyt projektu", "health-check", "project health". Use AFTER /10x-stack-assess (brownfield chain), BEFORE agent onboarding (m1-l4).
日本語の概要は準備中です。原文の説明を表示しています。
You MUST use this when building projects end-to-end. Orchestrates all 12 team roles — automatically switches between CTO, architect, PM, engineers, SRE, security, DBA, QA, and EM based on the current phase of work. Starts with brainstorming before any implementation.
日本語の概要は準備中です。原文の説明を表示しています。
Use when you need to add or configure Maven plugins in your pom.xml — including quality tools (enforcer, surefire, failsafe, jacoco, pitest, spotbugs, pmd), security scanning (OWASP), code formatting (Spotless), version management, container image build (Jib), build information tracking, and benchmarking (JMH) — through a consultative, modular step-by-step approach that only adds what you actually need. This should trigger for requests such as Add Maven plugins in pom.xml; Improve Maven plugins in pom.xml. Part of cursor-rules-java project
日本語の概要は準備中です。原文の説明を表示しています。