API Security Checklist
Giriş
Güvenlik, uygulamanızın kapısındaki kilit gibidir. En güzel evi yapabilirsiniz ama kapıyı açık bırakırsanız herkes girip çıkar. API güvenliği de tam olarak budur — uygulamanız dışarıya ne kadar açık, saldırganlar ne kadar içeri girebilir?
2023 yılında OWASP'ın raporuna göre, web uygulamalarına yapılan saldırıların %90'ından fazlası bilinen ve önlenebilir güvenlik açıklarından kaynaklanıyor. SQL Injection, kimlik doğrulama zafiyetleri, yetersiz yetkilendirme — bunların hepsi "biliyorduk ama yapmadık" kategorisindeki sorunlar.
Spring Boot ve Spring Security ekosistemi, bu tehditlerin çoğuna karşı güçlü savunma mekanizmaları sunar. Ancak bu araçları doğru kullanmak geliştiricinin sorumluluğundadır. Bu derste katmanlı güvenlik (defense in depth) yaklaşımıyla, bir Spring Boot API'sini production-ready güvenlik seviyesine nasıl getireceğimizi adım adım inceleyeceğiz.
1. Input Validation — Girdi Doğrulama
Her güvenlik stratejisinin ilk katmanı: dışarıdan gelen her veriye şüpheyle yaklaşmak. SQL injection, XSS ve command injection gibi saldırıların tamamı, doğrulanmamış kullanıcı girdisinden kaynaklanır.
Bean Validation
// Her endpoint'te @Valid kullanın — validasyonsuz endpoint olmamalı
@RestController
@RequestMapping("/api/v1/users")
@Validated // Path/Query param validasyonu için de gerekli
public class UserController {
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(userService.createUser(request));
}
@GetMapping
public ResponseEntity<Page<UserResponse>> searchUsers(
@RequestParam @Size(min = 1, max = 100) String query,
@RequestParam @Min(0) int page,
@RequestParam @Min(1) @Max(100) int size) {
return ResponseEntity.ok(
userService.search(query, page, size));
}
}public record CreateUserRequest(
@NotBlank(message = "Kullanıcı adı boş olamaz")
@Size(min = 3, max = 50, message = "Kullanıcı adı 3-50 karakter olmalı")
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
message = "Kullanıcı adı sadece harf, rakam, _ ve - içerebilir")
String username,
@NotBlank(message = "E-posta boş olamaz")
@Email(message = "Geçerli bir e-posta adresi giriniz")
String email,
@NotBlank(message = "Şifre boş olamaz")
@Size(min = 8, max = 128, message = "Şifre 8-128 karakter olmalı")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).*$",
message = "Şifre en az bir büyük harf, küçük harf, rakam ve özel karakter içermelidir")
String password,
@Size(max = 500, message = "Bio en fazla 500 karakter olabilir")
String bio,
@Past(message = "Doğum tarihi geçmişte olmalı")
LocalDate birthDate
) {}Custom Validator — Güvenli Metin
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeTextValidator.class)
public @interface SafeText {
String message() default "Metin güvenli olmayan karakterler içeriyor";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SafeTextValidator implements ConstraintValidator<SafeText, String> {
// SQL Injection ve XSS pattern'leri
private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
"(?i)((<script|javascript:|on\\w+=)|" + // XSS
"(union\\s+select|drop\\s+table|insert\\s+into|" + // SQL Injection
"delete\\s+from|update\\s+set|exec\\s*\\()|" +
"(\\$\\{|\\$\\(|`|\\|\\||&&))" // Command Injection
);
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true;
return !DANGEROUS_PATTERN.matcher(value).find();
}
}public record SearchRequest(
@SafeText
@Size(max = 200)
String query,
@SafeText
@Size(max = 50)
String category
) {}⚠️ Dikkat: Input validation tek başına SQL injection'ı engellemez. JPA/Hibernate parameterized query kullanır ve bu zaten SQL injection'a karşı temel savunmadır. Ancak doğrudan SQL yazdığınız yerlerde (native query, JDBC) mutlaka parameterized query kullanın, string concatenation ile SQL OLUŞTURMAYIN.
// ❌ YANLIŞ — SQL Injection açığı
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'",
nativeQuery = true)
// ✅ DOĞRU — Parameterized query
@Query(value = "SELECT * FROM users WHERE name = :name",
nativeQuery = true)
List<User> findByName(@Param("name") String name);2. Authentication — Kimlik Doğrulama
Kimlik doğrulama, "Sen kimsin?" sorusunun cevabıdır. Modern API'lerde en yaygın yöntem JWT (JSON Web Token) ve OAuth2'dir.
JWT Authentication
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
private final AuthenticationProvider authProvider;
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Stateless API'de CSRF gereksiz
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// Public endpoint'ler
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// Admin endpoint'leri
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// Diğer tüm istekler authenticate olmalı
.anyRequest().authenticated()
)
.authenticationProvider(authProvider)
.addFilterBefore(jwtFilter,
UsernamePasswordAuthenticationFilter.class)
.build();
}
}JWT Token Üretimi
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.access-token-expiration:900}") // 15 dakika
private long accessTokenExpiration;
@Value("${app.jwt.refresh-token-expiration:604800}") // 7 gün
private long refreshTokenExpiration;
public String generateAccessToken(UserDetails user) {
return buildToken(user, accessTokenExpiration, Map.of(
"type", "access",
"roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList()
));
}
public String generateRefreshToken(UserDetails user) {
return buildToken(user, refreshTokenExpiration, Map.of(
"type", "refresh"
));
}
private String buildToken(UserDetails user, long expirationSeconds,
Map<String, Object> extraClaims) {
Instant now = Instant.now();
return Jwts.builder()
.subject(user.getUsername())
.issuedAt(Date.from(now))
.expiration(Date.from(now.plusSeconds(expirationSeconds)))
.claims(extraClaims)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername())
&& !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration)
.before(new Date());
}
private <T> T extractClaim(String token,
Function<Claims, T> resolver) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return resolver.apply(claims);
}
}JWT Best Practices
app:
jwt:
secret: ${JWT_SECRET} # En az 256-bit, environment variable'dan
access-token-expiration: 900 # 15 dakika — kısa tutun
refresh-token-expiration: 604800 # 7 günAccess token kısa ömürlü (15-60 dakika): Çalınsa bile kısa sürede geçersiz olur
Refresh token rotation: Her kullanımda yeni refresh token ver, eskisini geçersiz kıl
Token'da hassas bilgi yok: JWT payload Base64 encoded, şifrelenmemiştir — herkes okuyabilir
Blacklist mekanizması: Çıkış yapan veya şüpheli token'ları Redis'te blacklist'e alın
3. Authorization — Yetkilendirme
Yetkilendirme, "Ne yapabilirsin?" sorusunun cevabıdır. Kimliği doğrulanmış bir kullanıcının hangi kaynaklara erişebileceğini belirler.
URL-Level Authorization
// SecurityFilterChain'de URL bazlı yetkilendirme
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/products/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/v1/products/**").hasRole("ADMIN")
.requestMatchers("/api/v1/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/reports/**").hasAuthority("REPORT_VIEW")
)Method-Level Authorization
@EnableMethodSecurity // Ana sınıfa ekleyin
@SpringBootApplication
public class MyApp {}
@Service
public class OrderService {
// Sadece ADMIN silebilir
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
// Siparişin sahibi veya ADMIN erişebilir
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id)")
public OrderResponse getOrder(Long id) { ... }
// Dönüş değerini filtrele — sadece kendi siparişlerini görsün
@PostAuthorize("returnObject.userId == authentication.principal.id " +
"or hasRole('ADMIN')")
public OrderResponse getOrderDetails(Long id) { ... }
// Koleksiyon filtreleme
@PostFilter("filterObject.userId == authentication.principal.id " +
"or hasRole('ADMIN')")
public List<OrderResponse> getAllOrders() { ... }
}IDOR (Insecure Direct Object Reference) Koruması
IDOR, en yaygın yetkilendirme açıklarından biridir. Kullanıcı, URL'deki ID'yi değiştirerek başkasının verisine erişir:
GET /api/v1/orders/42 → Kendi siparişi ✅
GET /api/v1/orders/43 → Başkasının siparişi ❌ (IDOR açığı!)@Component("orderSecurity")
@RequiredArgsConstructor
public class OrderSecurityEvaluator {
private final OrderRepository orderRepository;
public boolean isOwner(Long orderId) {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
Long currentUserId = ((UserPrincipal) auth.getPrincipal()).getId();
return orderRepository.findById(orderId)
.map(order -> order.getUserId().equals(currentUserId))
.orElse(false);
}
}
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id)")
public OrderResponse getOrder(Long id) {
return orderRepository.findById(id)
.map(orderMapper::toResponse)
.orElseThrow(() -> ResourceNotFoundException.order(id));
}
}4. Rate Limiting — İstek Sınırlama
Brute-force saldırıları, DDoS ve API abuse'u engellemek için istek sayısını sınırlayın:
// Bucket4j ile Rate Limiting
@Configuration
public class RateLimitConfig {
@Bean
public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
FilterRegistrationBean<RateLimitFilter> registrationBean =
new FilterRegistrationBean<>();
registrationBean.setFilter(new RateLimitFilter());
registrationBean.addUrlPatterns("/api/*");
registrationBean.setOrder(1);
return registrationBean;
}
}
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets =
new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String key = resolveKey(request);
Bucket bucket = buckets.computeIfAbsent(key,
k -> createBucket(request));
if (bucket.tryConsume(1)) {
// Rate limit headers ekle
response.addHeader("X-Rate-Limit-Remaining",
String.valueOf(bucket.getAvailableTokens()));
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType("application/json");
response.getWriter().write("""
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Çok fazla istek gönderdiniz. Lütfen bekleyin.",
"retryAfter": 60
}
""");
}
}
private Bucket createBucket(HttpServletRequest request) {
// Login endpoint'i daha sıkı sınırla (brute-force koruması)
if (request.getRequestURI().contains("/auth/login")) {
return Bucket.builder()
.addLimit(Bandwidth.simple(5, Duration.ofMinutes(1))) // 5/dk
.addLimit(Bandwidth.simple(20, Duration.ofHours(1))) // 20/saat
.build();
}
// Genel API limiti
return Bucket.builder()
.addLimit(Bandwidth.simple(100, Duration.ofMinutes(1)))
.build();
}
private String resolveKey(HttpServletRequest request) {
// Authenticated ise user ID, değilse IP
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return "user:" + auth.getName();
}
return "ip:" + request.getRemoteAddr();
}
}5. HTTPS ve Security Headers
HTTPS Zorlaması
# application-prod.yml
server:
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD}
key-store-type: PKCS12
port: 443
# HTTP → HTTPS yönlendirme
spring:
web:
resources:
add-mappings: falseSecurity Headers
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
return http
.headers(headers -> headers
// XSS koruması
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter
.HeaderValue.ENABLED_MODE_BLOCK))
// Content-Type sniffing engelleme
.contentTypeOptions(Customizer.withDefaults())
// Clickjacking koruması
.frameOptions(frame -> frame.deny())
// HSTS — tarayıcıyı HTTPS'e zorla
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)) // 1 yıl
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:;"))
// Referrer Policy
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter
.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
// Permissions Policy
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=()"))
)
// ... diğer konfigürasyonlar
.build();
}
}6. CORS Yapılandırması
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// ❌ YANLIŞ — her şeye izin vermek
// config.addAllowedOrigin("*");
// ✅ DOĞRU — sadece bilinen origin'lere izin
config.setAllowedOrigins(List.of(
"https://myapp.com",
"https://admin.myapp.com"
));
config.setAllowedMethods(List.of(
"GET", "POST", "PUT", "DELETE", "PATCH"));
config.setAllowedHeaders(List.of(
"Authorization", "Content-Type", "X-Request-Id"));
config.setExposedHeaders(List.of(
"X-Rate-Limit-Remaining", "X-Total-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L); // Preflight cache: 1 saat
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}⚠️ Dikkat:
allowedOrigins("*")ileallowCredentials(true)birlikte kullanılamaz. Bu güvenlik açığı oluşturur. Spesifik origin'ler belirtin.
7. CSRF Koruması
// Stateless (JWT) API'de CSRF kapatılır
.csrf(csrf -> csrf.disable())
// Session-based uygulamada CSRF aktif olmalı
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))Neden stateless API'de CSRF kapatılır? CSRF saldırısı, tarayıcının otomatik olarak cookie gönderme özelliğini kötüye kullanır. JWT token, Authorization header'ında gönderilir ve tarayıcı bunu otomatik göndermez — dolayısıyla CSRF riski yoktur.
8. Data Protection — Veri Koruma
Response'da Gereksiz Veri Döndürmeyin (DTO Pattern)
// ❌ KÖTÜ — Entity doğrudan döndürme
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
// password hash, internal notes, createdBy vs. HEPSİ gider!
}
// ✅ DOĞRU — DTO ile sadece gerekli alanlar
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow();
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getCreatedAt()
// password, internalNotes YOK
);
}Sensitive Data Maskeleme (Loglarda)
// ❌ KÖTÜ — hassas bilgi log'a yazılıyor
log.info("User login: email={}, password={}",
request.email(), request.password());
// ✅ DOĞRU — hassas bilgi maskeleniyor
log.info("User login attempt: email={}",
maskEmail(request.email()));
private String maskEmail(String email) {
int atIndex = email.indexOf('@');
if (atIndex <= 2) return "***@" + email.substring(atIndex + 1);
return email.substring(0, 2) + "***" + email.substring(atIndex);
}Password Hashing
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt — adaptif, salt otomatik, güvenli
return new BCryptPasswordEncoder(12); // strength: 12 rounds
}
}
// Kullanım
String hashed = passwordEncoder.encode(rawPassword);
boolean matches = passwordEncoder.matches(rawPassword, hashed);
// ASLA:
// - MD5 veya SHA-1 kullanmayın (kırılmış)
// - Salt'sız hash yapmayın
// - Şifreyi plain text saklamayın9. Dependency Scanning — Bağımlılık Güvenliği
Projenizin bağımlılıkları bilinen güvenlik açıkları (CVE) içerebilir:
<!-- OWASP Dependency-Check Maven Plugin -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.7</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS> <!-- CVSS 7+ → build fail -->
</configuration>
</plugin># CI/CD'de çalıştırın
./mvnw dependency-check:check
# Snyk alternatifi
snyk test --all-projects10. Audit Logging — Denetim Günlüğü
Kim ne yaptı, ne zaman yaptı — kritik operasyonları kayıt altına alın:
@Aspect
@Component
@Slf4j
public class AuditLogAspect {
@Around("@annotation(Audited)")
public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
String user = auth != null ? auth.getName() : "anonymous";
String method = joinPoint.getSignature().toShortString();
log.info("AUDIT | user={} | action={} | args={}",
user, method, sanitizeArgs(joinPoint.getArgs()));
Object result = joinPoint.proceed();
log.info("AUDIT | user={} | action={} | result=SUCCESS",
user, method);
return result;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {}
// Kullanım
@Service
public class UserService {
@Audited
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) { ... }
@Audited
public void changePassword(Long userId, String newPassword) { ... }
}Güvenlik Kontrol Listesi
Pre-Development
☐ HTTPS everywhere (HTTP istekleri redirect)
☐ Spring Security dependency ekli
☐ Security configuration sınıfı oluşturuldu
☐ CORS spesifik origin'lerle yapılandırıldı
Authentication
☐ JWT access token kısa ömürlü (15-60 dk)
☐ Refresh token rotation uygulandı
☐ Password BCrypt ile hash'leniyor (strength ≥ 10)
☐ Login endpoint'te rate limiting aktif (5/dk)
☐ Failed login attempt sayacı ve lockout mekanizması
Authorization
☐ URL-level authorization (SecurityFilterChain)
☐ Method-level authorization (@PreAuthorize, @PostAuthorize)
☐ IDOR koruması (object-level authorization)
☐ Admin endpoint'ler ayrı route prefix'te (/api/v1/admin/)
Input/Output
☐ Tüm endpoint'lerde @Valid
☐ DTO pattern — entity'ler doğrudan dönmüyor
☐ Parameterized query (native SQL'de)
☐ Response'da hassas bilgi yok (password, internal fields)
Infrastructure
☐ Security headers (HSTS, CSP, X-Frame-Options)
☐ OWASP Dependency-Check CI/CD'de çalışıyor
☐ Actuator endpoint'leri kısıtlı
☐ Secret'lar environment variable veya Vault'ta
☐ Audit logging aktif
Monitoring
☐ Failed authentication girişimleri loglanıyor
☐ Rate limit aşımları izleniyor
☐ 401/403 hataları alert oluşturuyor
☐ Dependency vulnerability tarama scheduled
Actuator Güvenliği
Spring Boot Actuator, production'da büyük güvenlik riski oluşturabilir:
# ❌ KÖTÜ — her şey açık
management:
endpoints:
web:
exposure:
include: "*"
# ✅ DOĞRU — sadece gerekli olanlar
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /internal/actuator # Standart path'i değiştirin
endpoint:
health:
show-details: when_authorized
env:
enabled: false # Ortam değişkenlerini ASLA expose etmeyin
configprops:
enabled: false # Konfigürasyonu expose etmeyin// Actuator endpoint'lerini ayrı port'ta çalıştırın
// application-prod.yml
management:
server:
port: 9090 # Ana port: 8080, Actuator: 9090
// Kubernetes/Docker'da 9090 portunu dışarıya açmayınYaygın Hatalar
1. "Güvenlik sonra ekleriz" — Her zaman baştan ekleyin
2. CORS'ta wildcard (*) kullanmak — Spesifik origin belirtin
3. JWT token'da hassas bilgi saklamak — Token şifrelenmez, herkes okuyabilir
4. Rate limiting olmadan login endpoint — Brute-force saldırısına açık
5. Actuator endpoint'leri açık bırakmak — /env ve /configprops ortam değişkenlerini ifşa eder
Özet
Defense in Depth: Güvenlik tek katmanlı değil — input validation + authentication + authorization + HTTPS + monitoring birlikte çalışmalı
Input Validation: Her girdiye şüpheyle yaklaşın,
@Valid+ custom validator kullanınJWT Best Practices: Kısa ömürlü access token, refresh token rotation, blacklist mekanizması
IDOR Koruması: URL-level yetmez, object-level authorization da şart (
@PreAuthorize+ custom evaluator)Rate Limiting: Özellikle login ve hassas endpoint'lerde brute-force koruması
Dependency Scanning: OWASP Dependency-Check ile bilinen CVE'leri CI/CD'de yakala
AI Asistan
Sorularını yanıtlamaya hazır