写在前面的

        本文主要提供一个日常工作中遇到的问题场景,讲解如何利用AI图片识别能力和游览器自动化控制能力,解决问题的思路和方法。代码为核心问题代码,并非全部代码,如果想跑起来,可以找作者要全部源码。

为什么会有这个需求?

  1. 作者公司有套gz网安的系统,会联动上级单位报警,但是这套系统没有出现告警发送邮件通知提醒功能,询问了厂商,需要将安全日志导入到自己的日志设备中,通过日志分析设备带的告警功能区实现。
  2. 作者单位有深信服的日志设备,配置syslog接收日志但是接收不到告警日志格式的,都是原始的syslog日志,显然就无用了,估计是厂商之间还是有些壁垒。
  3. 为了快速响应告警,避免出现告警事件,尤其是一些高等级事件,如失陷告警导致被上级单位通报,作者决定自己写个告警的小程序来解决这个需求。

功能实现的路径分析:

  1. Playwright 操作edge 游览器模拟登录
  2. 通过大模型识别验证码
  3. 分析告警页面信息,判断是否存在告警。
  4. 告警信息通过邮件发送到指定邮箱
  5. 告警信息通过企业微信机器人发送到指定群

具体的代码实现过程和需要解决的问题

一、初始化playwright,并访问奇安信设备的管理页面:

  1. 首先需要安装  Microsoft.Playwright 包
  2. 初始化playwright ,context, 打开登录页面
    
    string loginUrl = "设备访问页面";
    LogService.Initialize();
    LogService.Logger.Info("应用程序启动");
    var playwright = await Playwright.CreateAsync();
    await using var context = await playwright.Chromium.LaunchPersistentContextAsync(
                    @"C:\EdgeUserData",   //自定义的用户数据存放目录
                    new BrowserTypeLaunchPersistentContextOptions
                    {
                        Headless = false,
                        Channel = "msedge",
                        Args = new[]
           {
                   "--disable-gpu",
                            "--no-sandbox",
                            "--disable-dev-shm-usage",
                            "--disable-blink-features=AutomationControlled"
            }
                    });
    var page = context.Pages[0];
    await page.SetViewportSizeAsync(1920, 1080);
    await page.GotoAsync(q.loginUrl);
    page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);  

二、分析定位页面元素

 1. 整个页面需要获取到用户名输入框,密码输入框,验证码输入框,验证码图片,立即登录按钮四个元素。

        按F12 打开开发者工具可以查看页面结构,进入到元素的页面。

这里有个小技巧可以点住页面圆满,按ctrl+F 调出查找页面,这样能够快速定位元素

2. 页面结构分析的结果如下:

用户名: input[id='name']
密码: input[id='password']

验证码框:input[id=\"captcha\"]

登录按钮:button[id='btn_submit']

验证码图片:img[id='captcha_img_id']

这里有个需要解决的问题,验证码的图片看下页面代码如下:

<img id="captcha_img_id" name="captcha_img" src="/verify.html" alt="CAPTCHA" onclick="change_captcha();">

验证码的图片是 src =‘/verify.html ’  , 这个页面每次访问图片都会变动的,所以必须调用playwright的截图功能,来获取二维码的图片。核心代码如下:

  var checkCodeDiv = await page.QuerySelectorAsync("img[id='captcha_img_id']");
  if (checkCodeDiv != null)
  {
      var box = await checkCodeDiv.BoundingBoxAsync();
      if (box != null)
      {
          await page.ScreenshotAsync(new PageScreenshotOptions
          {
              Path = @".\png\code.png",
              Clip = new Clip
              {
                  X = box.X,
                  Y = box.Y,
                  Width = box.Width,
                  Height = box.Height
              }
          });
      }


  }

3.  拿到了验证码的图片,下一步就需要将验证码发送给大模型去做解析。

        这一步有点复杂,作者是本地使用lmstudio, 跑了qwen3.5  35B的模型,注意关闭模型的思考,速度会很,秒响应。硬件是一块4090的显卡+ 4060ti,24G+16G=40G显存。对于这种跑验证码,不涉及的公司内部信息,也可以直接去接外部的大模型,虽然有些费用。提示词是“请识别验证码,直接返回文本,内容为4位数字或字母,没有其他字符”,lmstudio支持标准的openai接口的,拿到的返回代码,只需要里面的“content”部分内容。 这一部分具体的不细讲。

4. 获取到验证码,那么可以点击按钮进行登录。

        这里需要考虑两个问题,一个是如何判断登录成功,也就是AI返回的验证码是否正确(默认用户名和密码是正确的),一个是需要有重试的机制。

        a.  如何判断你验证码正确:

        如果验证码输入错误,页面是不会调整,同时页面会出现一个tip提示,可以把页面是否有该元素作为验证码是否成功的标志。

        b. 重试的机制

        写一个函数,把整个页面用户名、密码、验证码分析以及按钮点击都放在里面,页面登录成功后返回验证码的字符串,失败返回空字符串。(作者想保留下来识别结果做他用,返回true或者false也都可以)

        定义一个重试次数的变量,用while循环来判断是该变量超过阈值,如果页面登录失败,那么该变量自增,如果成功,那么跳出while循环。

     public async  Task<String>  TaskLoginAction(IPage page) {
         var userNameInput = await page.QuerySelectorAsync("input[id='name']");
         await userNameInput.FillAsync(userName);
         var passwordInput = await page.QuerySelectorAsync("input[id='password']");
         await passwordInput.FillAsync(password);
         var checkCodeDiv = await page.QuerySelectorAsync("img[id='captcha_img_id']");
         if (checkCodeDiv != null)
         {
             var box = await checkCodeDiv.BoundingBoxAsync();
             if (box != null)
             {
                 await page.ScreenshotAsync(new PageScreenshotOptions
                 {
                     Path = @".\png\code.png",
                     Clip = new Clip
                     {
                         X = box.X,
                         Y = box.Y,
                         Width = box.Width,
                         Height = box.Height
                     }
                 });
             }

         }
         if (File.Exists(@".\png\code.png"))
         {
             byte[] imageBytes = File.ReadAllBytes(@".\png\code.png");
             string base64String = Convert.ToBase64String(imageBytes);
             string url = $"data:image/png;base64,{base64String}";
             string result = await client.RecognizeCaptchaFromBase64(url);
             var checkCodeInput = await page.QuerySelectorAsync("input[id=\"captcha\"]");
             await checkCodeInput.FillAsync(result);
             var submitButton = await page.QuerySelectorAsync("button[id='btn_submit']");
             await submitButton.ClickAsync();
             Thread.Sleep(1000);
             var tipSpan = await page.QuerySelectorAsync("span[class='tip']");
             if (tipSpan != null)
             {
                 return "";
             }
             else
             {
                 return result;

             }
         }
         else { 
         
             return "";
         }
         }

重试机制代码

    string code = await TaskLoginAction(page);
    int rerTryTimes = 1;
    if (!string.IsNullOrEmpty(code))
    {
        File.Move(@".\png\code.png", $".\\png\\{code}.png");
        LogService.Logger.Info("登录成功");

    }
    else {
        while (rerTryTimes < 10 && string.IsNullOrEmpty(code)) {
            page.ReloadAsync();
            Thread.Sleep(1000); 
            code = await TaskLoginAction(page);
            if (!string.IsNullOrEmpty(code))
            {
                File.Move(@".\png\code.png", $".\\png\\{code}.png");
                LogService.Logger.Info("登录成功");


            }
            rerTryTimes++;
        }

    }

5. 登录成功后,点击数据中心,默认就是威胁日志,这个就是我们需要的内容。

        分析页面,数据中心按钮对应 a标签的路径为:  //a[text()='数据中心']

6. 分析日志, 关注类型是  攻击结果为 “失陷”; 威胁类型为: "恶意代码", "恶意邮件","漏洞攻击","入侵攻击"

        两种方式可以获取日志,一种是在下来菜单做选择,一种是查询输入框中输入指令来查询,显然后者要简单些。分析页面,查询框、查询按钮、以及结果对应的元素如下:

查询框: input[id='tbar_monitor_logThreat_input']

查询按钮: div[id='tbar_monitor_logThreat_input-textfield-search']

查询结果: div[id*=\"monitor_logThreat_maingrid-rowdata\"]

查询结果是多行,我们使用 QuerySelectorAllAsync ,如果返回的结果元素多个表明存在该查询类型的日志,那么该类型需要告警。注意失陷对应的是结果 attack_status  , 而其他的是  threat_type

失陷:((attack_status eq '失陷'))

入侵攻击: ((threat_type eq '入侵攻击'))

#region  事件检测
public async Task<JObject> CheckIfExitsAlert(IPage page, string alertTypeString, string type) { 
    JObject  resObj = new JObject();
    resObj.Add("alertTypeString", alertTypeString);
    var inputText = await page.QuerySelectorAsync("input[id='tbar_monitor_logThreat_input']");
    await inputText.FillAsync($"(({type} eq '{alertTypeString}'))");
    var queryButton = await page.QuerySelectorAsync("div[id='tbar_monitor_logThreat_input-textfield-search']");
    await queryButton.ClickAsync();
    Thread.Sleep(2000);
    var threatDiv = await page.QuerySelectorAllAsync("div[id*=\"monitor_logThreat_maingrid-rowdata\"]");
    if (threatDiv.Count == 0) {
        resObj.Add("ifAlert", false);
        resObj.Add("snapPng", "");
        LogService.Logger.Info($"没有{alertTypeString}");
    }
    else{
        resObj.Add("ifAlert", true);
        string pngName = $"{alertTypeString}-{DateTime.Now.ToString("yyyyMMddHHssmm")}.png";
        await page.ScreenshotAsync(new PageScreenshotOptions
        {
            Path = $".\\风险截图\\{pngName}"
        });
        if (File.Exists($".\\风险截图\\{pngName}")) {
            resObj.Add("snapPng", pngName);
        }
        LogService.Logger.Info($"存在{alertTypeString}");
    }

    return resObj;
}


#endregion

7. 拿到了日志告警的结果,那么发送邮件和企业微信告警都是简单的事情,这部分代码都可以交给AI自动去生成。如果要定时巡检,那么可以设置定时任务来执行程序。

写在最后:

       在企业AI落地过程中,更多可以考虑在提高效率,减少重复性劳动,通过这个小程序,作者就不用每日都打开安全设备管理页面去查看,后续作者还计划将该功能生成copaw的技能,让copaw定时调用,输出报告到企业微信中。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐